First pass conversion of ant build over to maven.

git-svn-id: https://svn.apache.org/repos/asf/tapestry/tapestry3/trunk@671206 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/build.xml b/build.xml
index 42eed76..2da4124 100644
--- a/build.xml
+++ b/build.xml
@@ -44,7 +44,7 @@
 	
 	<path id="vlib.classpath">
 		<pathelement location="${vlibbeans.jar}"/>
-		<pathelement location="examples/VlibBeans/jboss"/>
+		<pathelement location="tapestry-examples/VlibBeans/jboss"/>
 		<pathelement location="${jboss.client.dir}/jboss-j2ee.jar"/>
 		<pathelement location="${jboss.client.dir}/jboss-client.jar"/>
 		<pathelement location="${jboss.client.dir}/jnp-client.jar"/>
@@ -55,7 +55,7 @@
 		<ant dir="framework" target="clean"/>
 		<ant dir="contrib" target="clean"/>
 		<ant dir="junit" target="clean"/>
-		<ant dir="examples" target="clean"/>
+		<ant dir="tapestry-examples" target="clean"/>
 		<ant dir="doc/src" target="clean"/>
 		<delete dir="${private.dir}" quiet="true"/>		
 	</target>
@@ -80,19 +80,19 @@
      	 doesn't override definitions in each project's buildfile. -->
 		<ant dir="framework" target="install" inheritAll="false"/>
 		<ant dir="contrib" target="install" inheritAll="false"/>
-		<ant dir="examples" target="install" inheritAll="false"/>
+		<ant dir="tapestry-examples" target="install" inheritAll="false"/>
 	</target>
 	
 	<target name="build-workbench" description="Builds and installs the Workbench demo."
 		depends="download-ext-framework">
-		<ant dir="examples/Workbench" target="install" inheritAll="false"/>
+		<ant dir="tapestry-examples/tapestry-workbench" target="install" inheritAll="false"/>
 	</target>
 	
 	<target name="build-vlib" description="Builds and installs the Virtual Library demo."
 		depends="download-ext-framework,check-for-jboss-dir">
-		<ant dir="examples/VlibBeans" target="install" inheritAll="false"/>
-		<ant dir="examples/Vlib" target="install" inheritAll="false"/>
-		<ant dir="examples/VlibEAR" target="install" inheritAll="false"/>		
+		<ant dir="tapestry-examples/VlibBeans" target="install" inheritAll="false"/>
+		<ant dir="tapestry-examples/Vlib" target="install" inheritAll="false"/>
+		<ant dir="tapestry-examples/VlibEAR" target="install" inheritAll="false"/>
 	</target>
 	
 	<target name="documentation" depends="download-ext-doc"
diff --git a/doc/src/ComponentReference/ActionLink.html b/doc/src/ComponentReference/ActionLink.html
index b5198aa..2eddbaa 100644
--- a/doc/src/ComponentReference/ActionLink.html
+++ b/doc/src/ComponentReference/ActionLink.html
@@ -164,7 +164,7 @@
   from the Customer List table.
 	<p>
 
-<table class="examples" cellspacing="6">
+<table class="tapestry-examples" cellspacing="6">
   <tr align="left">
    <th>ID</th>
    <th>&nbsp;</th>
diff --git a/doc/src/ComponentReference/Any.html b/doc/src/ComponentReference/Any.html
index f3fe443..cae0c80 100644
--- a/doc/src/ComponentReference/Any.html
+++ b/doc/src/ComponentReference/Any.html
@@ -104,7 +104,7 @@
   In this example the Any component is use to generate XML order list document. 
 	<p>
 
-<table class="examples" cellpadding="8">
+<table class="tapestry-examples" cellpadding="8">
 <tr>
  <td>
 <font color="blue">&lt;?xml version="1.0" encoding="ISO-8859-1" ?&gt;</font><br> 
diff --git a/doc/src/ComponentReference/Checkbox.html b/doc/src/ComponentReference/Checkbox.html
index fc34dcc..f468699 100644
--- a/doc/src/ComponentReference/Checkbox.html
+++ b/doc/src/ComponentReference/Checkbox.html
@@ -117,7 +117,7 @@
 	Provides a checkbox for the user contact a sales representative. 
 	<p>
 	
-<table class="examples" cellspacing="8">
+<table class="tapestry-examples" cellspacing="8">
  <tr>
 	<td>
 	 <input type="checkbox" checked> Contact Sales Rep
diff --git a/doc/src/ComponentReference/Conditional.html b/doc/src/ComponentReference/Conditional.html
index 12fd7f3..74728ed 100644
--- a/doc/src/ComponentReference/Conditional.html
+++ b/doc/src/ComponentReference/Conditional.html
@@ -129,7 +129,7 @@
   person is a manager and if they are a manager whether they have any staff.	
 	<p>
 	
-<table class="examples" cellpadding="8">
+<table class="tapestry-examples" cellpadding="8">
 <tr>
  <td>
 John Smith is a Manager with <font color="red"><b>no</b></font> staff. 	
diff --git a/doc/src/ComponentReference/DatePicker.html b/doc/src/ComponentReference/DatePicker.html
index bcbb8ee..5d1d54d 100644
--- a/doc/src/ComponentReference/DatePicker.html
+++ b/doc/src/ComponentReference/DatePicker.html
@@ -155,7 +155,7 @@
   an end date.
   </p>
 
-<table class="examples" cellpadding="8">
+<table class="tapestry-examples" cellpadding="8">
 <tr>
  <td>
 <form jwcid="form">
diff --git a/doc/src/ComponentReference/DirectLink.html b/doc/src/ComponentReference/DirectLink.html
index 4a4d76a..70cafdc 100644
--- a/doc/src/ComponentReference/DirectLink.html
+++ b/doc/src/ComponentReference/DirectLink.html
@@ -195,7 +195,7 @@
   Customers from a Customer List table.
 	<p>
 
-<table class="examples" cellspacing="6">
+<table class="tapestry-examples" cellspacing="6">
   <tr align="left">
    <th>ID</th>
    <th>&nbsp;</th>
diff --git a/doc/src/ComponentReference/Foreach.html b/doc/src/ComponentReference/Foreach.html
index a641555..a679204 100644
--- a/doc/src/ComponentReference/Foreach.html
+++ b/doc/src/ComponentReference/Foreach.html
@@ -150,7 +150,7 @@
 	The Foreach component is used to generate a table from a Customer List.
 	<p>
 
-<table class="examples" cellspacing="6">
+<table class="tapestry-examples" cellspacing="6">
   <tr align="left">
    <th>ID</th>
    <th>&nbsp;</th>
diff --git a/doc/src/ComponentReference/Form.html b/doc/src/ComponentReference/Form.html
index c363b35..fac2802 100644
--- a/doc/src/ComponentReference/Form.html
+++ b/doc/src/ComponentReference/Form.html
@@ -178,7 +178,7 @@
 	The Form component is used to provide a simple login page.
 	<p>
 
-<table class="examples" cellpadding="4">
+<table class="tapestry-examples" cellpadding="4">
 <form>
  <tr>
   <td>Username:</td><td><input size="12"></td>
diff --git a/doc/src/ComponentReference/Image.html b/doc/src/ComponentReference/Image.html
index b3c3513..31dec24 100644
--- a/doc/src/ComponentReference/Image.html
+++ b/doc/src/ComponentReference/Image.html
@@ -111,7 +111,7 @@
 &lt;context-asset&gt; to reference the image. 
 	<p>
 
-<table class="examples" cellpadding="8" valign="middle">
+<table class="tapestry-examples" cellpadding="8" valign="middle">
 <tr>
  <td>
   <a href="http://tapestry.sourceforge.net/">
@@ -150,7 +150,7 @@
 <tt>ExternalAsset</tt></a> to reference the image's URL. 
 	<p>
 
-<table class="examples" cellpadding="8">
+<table class="tapestry-examples" cellpadding="8">
 <tr>
  <td>
   <h4>Elvis helitanker Saves 14 Lives</h4>
diff --git a/doc/src/ComponentReference/Insert.html b/doc/src/ComponentReference/Insert.html
index a287c28..2add08e 100644
--- a/doc/src/ComponentReference/Insert.html
+++ b/doc/src/ComponentReference/Insert.html
@@ -160,7 +160,7 @@
 	Inserts the pages dueDate and applies the specified DateFormat and HTML class.  
 	<p>
 
-<table class="examples" cellpadding="8">
+<table class="tapestry-examples" cellpadding="8">
 <tr>
  <td>
  The order was due on the <font color="red"><b>21 January 2002</b></font>. 	
diff --git a/doc/src/ComponentReference/PageLink.html b/doc/src/ComponentReference/PageLink.html
index 9c5f2c9..3169fcd 100644
--- a/doc/src/ComponentReference/PageLink.html
+++ b/doc/src/ComponentReference/PageLink.html
@@ -177,7 +177,7 @@
    <td><font color="white"><b><a href="PageLink.html">Logout</a></b></font></td>
  </tr>
 </table>
-<table class="examples" cellpadding="6" width="100%">
+<table class="tapestry-examples" cellpadding="6" width="100%">
  <tr>
    <td>My mail page content.</td>
  </tr>
diff --git a/doc/src/ComponentReference/PropertySelection.html b/doc/src/ComponentReference/PropertySelection.html
index b63ac03..6161220 100644
--- a/doc/src/ComponentReference/PropertySelection.html
+++ b/doc/src/ComponentReference/PropertySelection.html
@@ -173,7 +173,7 @@
   <a href="../api/org/apache/tapestry/form/StringPropertySelectionModel.html"><tt>StringPropertySelectionModel<tt></tt></tt></a>
             <tt><tt> 
             <p> 
-            <table class="examples" valign="middle" cellspacing="8">
+            <table class="tapestry-examples" valign="middle" cellspacing="8">
               <tr> 
                 <td>Gender: 
                   <select>
@@ -218,7 +218,7 @@
               and the new cloting item information is displayed by the description, 
               label and price <a href="Insert.html">Insert</a> components. 
             <p> 
-            <table class="examples" valign="middle" cellspacing="8">
+            <table class="tapestry-examples" valign="middle" cellspacing="8">
               <tr> 
                 <td>Item: 
                   <select>
diff --git a/doc/src/ComponentReference/RadioGroup.html b/doc/src/ComponentReference/RadioGroup.html
index b51ebd4..3402e9d 100644
--- a/doc/src/ComponentReference/RadioGroup.html
+++ b/doc/src/ComponentReference/RadioGroup.html
@@ -133,7 +133,7 @@
   &lt;fieldset&gt;&lt;legend&gt;..&nbsp;&lt;/legend&gt;..&nbsp;&lt;/fieldset&gt; tags.
   <p/>
       
-<table class="examples" cellpadding="4" cellspacing="4">
+<table class="tapestry-examples" cellpadding="4" cellspacing="4">
  <tr>
   <td>
    <fieldset><legend>Order Size</legend>
diff --git a/doc/src/ComponentReference/RenderBlock.html b/doc/src/ComponentReference/RenderBlock.html
index 2ad72d7..4a45db9 100644
--- a/doc/src/ComponentReference/RenderBlock.html
+++ b/doc/src/ComponentReference/RenderBlock.html
@@ -113,7 +113,7 @@
 	        <p> This example shows a page with a custom TabPanel component. When 
               a user selects a tab, TabPanel switches content. Each tab content 
               is defined by a Block.</p>
-			<table class="examples" border="0" width="350" cellspacing="0" cellpadding="0">
+			<table class="tapestry-examples" border="0" width="350" cellspacing="0" cellpadding="0">
 <tr>
   <td width="10">&nbsp;</td>
   <td>
diff --git a/doc/src/ComponentReference/RenderBody.html b/doc/src/ComponentReference/RenderBody.html
index f93f95b..61d8ea0 100644
--- a/doc/src/ComponentReference/RenderBody.html
+++ b/doc/src/ComponentReference/RenderBody.html
@@ -107,7 +107,7 @@
               a Border component to provide common layout to almost all of application 
               pages. </p>
              
-	        <table width="200" class="examples" cellspacing="0" border="0" align="center">
+	        <table width="200" class="tapestry-examples" cellspacing="0" border="0" align="center">
               <tr>
 				<td valign="top" style="text-align:justify;">
 <H1 align=center><FONT color=#ff3333>Agnosis</FONT></H1>
@@ -123,7 +123,7 @@
 				<BR/>
 				<B>O</B>lvidados por el Hado
 				<BR/>
-				<B>S</B>i Él existe
+				<B>S</B>i �l existe
 				<BR/>
 				<B>I</B>ncomprensible y eterno
 				<BR/>
diff --git a/doc/src/ComponentReference/Script.html b/doc/src/ComponentReference/Script.html
index 94aa8e6..b5b4a2a 100644
--- a/doc/src/ComponentReference/Script.html
+++ b/doc/src/ComponentReference/Script.html
@@ -146,7 +146,7 @@
   <tt>${expression}</tt> syntax.
 	<p>
 
-<table class="examples" cellpadding="4">
+<table class="tapestry-examples" cellpadding="4">
 <form>
 
  <tr>
diff --git a/doc/src/ComponentReference/Select.html b/doc/src/ComponentReference/Select.html
index 8c2322c..b0ee45a 100644
--- a/doc/src/ComponentReference/Select.html
+++ b/doc/src/ComponentReference/Select.html
@@ -139,7 +139,7 @@
 	with only a single component (instead of three, as will be shown below).
 	<p>
 
-<table class="examples" cellpadding="4">
+<table class="tapestry-examples" cellpadding="4">
  <tr>
   <td>Select a color
 	 <select name="selection" multiple>		
diff --git a/doc/src/ComponentReference/Shell.html b/doc/src/ComponentReference/Shell.html
index 539e95e..5d4097d 100644
--- a/doc/src/ComponentReference/Shell.html
+++ b/doc/src/ComponentReference/Shell.html
@@ -220,7 +220,7 @@
   path to resolve the stylesheet.
 	<p>
 
-<table class="examples" cellpadding="24">
+<table class="tapestry-examples" cellpadding="24">
 <tr>
  <td>
   <h2><i><font color="navy">Customer Login</font></i></h2>
diff --git a/doc/src/ComponentReference/Submit.html b/doc/src/ComponentReference/Submit.html
index c0f558a..ec177c0 100644
--- a/doc/src/ComponentReference/Submit.html
+++ b/doc/src/ComponentReference/Submit.html
@@ -190,7 +190,7 @@
   .
 	<p>
 
-<table class="examples" cellpadding="4">
+<table class="tapestry-examples" cellpadding="4">
 <form>
  <tr>
   <td>Username:</td><td><input size="12"></td>
@@ -262,7 +262,7 @@
  <A href="PropertySelection.html">PropertySelection</a>. 
  <p>
 
-<table class="examples" valign="middle" cellspacing="8">
+<table class="tapestry-examples" valign="middle" cellspacing="8">
  <tr>
   <td>
   <form>
diff --git a/doc/src/ComponentReference/TextArea.html b/doc/src/ComponentReference/TextArea.html
index 3839e14..d6849bd 100644
--- a/doc/src/ComponentReference/TextArea.html
+++ b/doc/src/ComponentReference/TextArea.html
@@ -124,7 +124,7 @@
   feedback &lt;textarea&gt;.
 	<p>
 
-  <table class="examples" valign="middle" cellspacing="8">
+  <table class="tapestry-examples" valign="middle" cellspacing="8">
 	 <tr>
 	  <td>Your Comments</td>
 	  <td><TEXTAREA name=textarea rows=5>Please enter your comments here</TEXTAREA>
diff --git a/doc/src/ComponentReference/TextField.html b/doc/src/ComponentReference/TextField.html
index 59699a8..532fb93 100644
--- a/doc/src/ComponentReference/TextField.html
+++ b/doc/src/ComponentReference/TextField.html
@@ -130,7 +130,7 @@
   form input field.
 	<p>
 
-<table class="examples" cellspacing="8" valign="middle">
+<table class="tapestry-examples" cellspacing="8" valign="middle">
  <tr>
   <td>Id</td>
   <td><input size="8" value="1245"></td>
diff --git a/doc/src/ComponentReference/Upload.html b/doc/src/ComponentReference/Upload.html
index 22dd12a..fa6eeb9 100644
--- a/doc/src/ComponentReference/Upload.html
+++ b/doc/src/ComponentReference/Upload.html
@@ -134,7 +134,7 @@
   obtain the web application's directory path.
 
 <form>
-<table class="examples" cellpadding="4">
+<table class="tapestry-examples" cellpadding="4">
 <tr>
  <td colspan="2">File:&nbsp;<input type="file"></td>
 </tr>
diff --git a/doc/src/ComponentReference/ValidField.html b/doc/src/ComponentReference/ValidField.html
index 34553a9..88f53c9 100644
--- a/doc/src/ComponentReference/ValidField.html
+++ b/doc/src/ComponentReference/ValidField.html
@@ -226,7 +226,7 @@
 	<p>
 
 <form>
-<table class="examples" cellpadding="2">
+<table class="tapestry-examples" cellpadding="2">
 <tr>
  <td colspan="2"><font color="navy" size="+2"><i><b>Regal Auctions Bid Page</b></i></font></td>
 </tr>
diff --git a/doc/src/ComponentReference/contrib.InspectorButton.html b/doc/src/ComponentReference/contrib.InspectorButton.html
index 8b44773..d1a3d13 100644
--- a/doc/src/ComponentReference/contrib.InspectorButton.html
+++ b/doc/src/ComponentReference/contrib.InspectorButton.html
@@ -117,7 +117,7 @@
 	<p>
   This example is a simple page with the InspectorButton.</p>
       
-<table class="examples" cellpadding="8">
+<table class="tapestry-examples" cellpadding="8">
 <tr>
  <td>
 <h1>Hello world</h1>
diff --git a/doc/src/ComponentReference/contrib.PopupLink.html b/doc/src/ComponentReference/contrib.PopupLink.html
index 7ccb396..893f0f3 100644
--- a/doc/src/ComponentReference/contrib.PopupLink.html
+++ b/doc/src/ComponentReference/contrib.PopupLink.html
@@ -123,7 +123,7 @@
   This example provides a context help popup link for an account number field. 
   <p>
 
-<table class="examples" cellspacing="8">
+<table class="tapestry-examples" cellspacing="8">
 <tr>
  <td>Account Number:</td>
  <td><input type="text"/></td>
diff --git a/doc/src/ComponentReference/contrib.Table.html b/doc/src/ComponentReference/contrib.Table.html
index 51a1c08..2043adb 100644
--- a/doc/src/ComponentReference/contrib.Table.html
+++ b/doc/src/ComponentReference/contrib.Table.html
@@ -474,7 +474,7 @@
   <b>Examples</b>
   <p>
 
-<table class="examples" cellpadding="4" width="700">
+<table class="tapestry-examples" cellpadding="4" width="700">
 <tr><td>
 
 <SPAN><A 
diff --git a/examples/Workbench/build.xml b/examples/Workbench/build.xml
deleted file mode 100644
index d213cd9..0000000
--- a/examples/Workbench/build.xml
+++ /dev/null
@@ -1,76 +0,0 @@
-<?xml version="1.0"?>
-<!-- $Id$ -->
-<project name="Tapestry Workbench Example" default="install">
-	<property name="root.dir" value="../.."/>
-	<property file="${root.dir}/config/Version.properties"/>
-	<property file="${root.dir}/config/build.properties"/>
-	<property file="${root.dir}/config/common.properties"/>
-	
-	<property name="config.dir" value="config"/>
-	<property name="build.dir" value=".build"/>
-	
-	<path id="compile.classpath">
-		<fileset dir="${root.lib.dir}">
-			<include name="*.jar"/>
-			<include name="${ext.dir}/*.jar"/>
-			<include name="${j2ee.dir}/*.jar"/>
-		</fileset>
-		<fileset dir="${lib.dir}">
-			<include name="*.jar"/>	
-		</fileset>
-	</path>
-	<target name="init">
-		<mkdir dir="${classes.dir}"/>
-	</target>
-	<target name="clean">
-		<delete dir="${classes.dir}" quiet="true"/>
-		<delete dir="${build.dir}" quiet="true"/>
-	</target>
-	<target name="compile" depends="init"
-		description="Compile all classes in the tutorial.">
-		<javac srcdir="${src.dir}" destdir="${classes.dir}" debug="on"
-			target="1.1" source="1.3">
-			<classpath refid="compile.classpath"/>
-		</javac>
-	</target>
-	<target name="install" depends="compile"
-		description="Compile all classes and build the installed WAR.">
-		
-		<mkdir dir="${build.dir}"/>
-		<mkdir dir="${examples.dir}"/>
-		
-		<copy file="${config.dir}/web.xml" todir="${build.dir}">
-			<filterset>
-				<filter token="TAPESTRY_JAR" value="${framework.jar}"/>
-			</filterset>
-		</copy>
-		
-		<war warfile="${examples.dir}/${workbench.war}"
-			webxml="${build.dir}/web.xml">
-			
-			<fileset dir="context"/>
-						
-			<classes dir="${classes.dir}"/>
-			<classes dir="${src.dir}">
-				<exclude name="**/*.java"/>
-				<exclude name="**/package.html"/>
-			</classes>
-			<classes dir="${root.config.dir}">
-			  <include name="log4j.properties"/>
-			</classes>
-			<lib dir="${lib.dir}">
-				<include name="*.jar"/>
-			</lib>
-			<lib dir="${root.lib.dir}">
-				<include name="*.jar"/>
-			</lib>			
-			<lib dir="${root.lib.dir}/${ext.dir}">
-			  <include name="*.jar"/>
-			</lib>
-			<lib dir="${root.lib.dir}/${runtime.dir}">
-			  <include name="*.jar"/>
-			</lib>
-		</war>
-	</target>
-	
-</project>
diff --git a/examples/Workbench/config/web.xml b/examples/Workbench/config/web.xml
deleted file mode 100644
index 48004f0..0000000
--- a/examples/Workbench/config/web.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-<?xml version="1.0"?>
-<!--$Id$ -->
-<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
- "http://java.sun.com/dtd/web-app_2_3.dtd">
-<web-app>
-  <display-name>Tapestry Workbench Example</display-name>
-  
-	<filter>
-		<filter-name>redirect</filter-name>
-		<filter-class>org.apache.tapestry.RedirectFilter</filter-class>
-	</filter>
-	
-	<filter-mapping>
-		<filter-name>redirect</filter-name>
-		<url-pattern>/</url-pattern>
-	</filter-mapping>
-  
-  <servlet>
-  	<servlet-name>workbench</servlet-name>
-    <servlet-class>org.apache.tapestry.ApplicationServlet</servlet-class>
-  	<load-on-startup>0</load-on-startup>
-  </servlet>
-     
-  <servlet-mapping>
-  	<servlet-name>workbench</servlet-name>
-  	<url-pattern>/app</url-pattern>
-  </servlet-mapping>  
-
-  <session-config>
-  	<session-timeout>15</session-timeout>
-  </session-config>
-  
-  <taglib>
-  	<taglib-uri>http://jakarta.apache.org/tapestry/tld/tapestry_1_0.tld</taglib-uri>
-  	<taglib-location>/WEB-INF/lib/@TAPESTRY_JAR@</taglib-location>
-  </taglib>
-</web-app>
diff --git a/examples/build.xml b/examples/build.xml
deleted file mode 100644
index c024d54..0000000
--- a/examples/build.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-<?xml version="1.0"?>
-<!-- Interface between the top-level build file and any of the examples.  Simply re-executes 
-     its targets in each of its sub-projects. -->
-<project name="Tapestry Examples" default="install">
-	<target name="clean">
-		<ant dir="Workbench" target="clean"/>
-		<ant dir="VlibBeans" target="clean"/>
-		<ant dir="Vlib" target="clean"/>
-	</target>
-	<target name="install">
-		<ant dir="Workbench" target="install" inheritAll="false"/>
-		<ant dir="VlibBeans" target="install" inheritAll="false"/>
-		<ant dir="Vlib" target="install" inheritAll="false"/>
-		<ant dir="VlibEAR" target="install" inheritAll="false"/>
-	</target>
-</project>
diff --git a/framework/src/org/apache/tapestry/enhance/javassist/ClassFabricator.java b/framework/src/org/apache/tapestry/enhance/javassist/ClassFabricator.java
deleted file mode 100644
index 0fa5093..0000000
--- a/framework/src/org/apache/tapestry/enhance/javassist/ClassFabricator.java
+++ /dev/null
@@ -1,295 +0,0 @@
-//  Copyright 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.tapestry.enhance.javassist;
-
-import java.io.IOException;
-import java.text.MessageFormat;
-
-import javassist.CannotCompileException;
-import javassist.ClassPool;
-import javassist.CtClass;
-import javassist.CtField;
-import javassist.CtMethod;
-import javassist.NotFoundException;
-
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.apache.tapestry.enhance.CodeGenerationException;
-
-/**
- *  @author Mindbridge
- *  @version $Id$
- *  @since 3.0
- */
-public class ClassFabricator
-{
-    private static final Log LOG = LogFactory.getLog(ClassFabricator.class);
-
-    /**
-     *  The code template for the standard property accessor method.    
-     *                                      <p>
-     *  Legend:                             <br>
-     *      {0} = property field name       <br>
-     */
-    private static final String PROPERTY_ACCESSOR_TEMPLATE = "" +
-        "'{'" +
-        "  return {0}; " +
-        "'}'";
-        
-    /**
-     *  The code template for the standard property mutator method.    
-     *                                      <p>
-     *  Legend:                             <br>
-     *      {0} = property field name       <br>
-     */
-    private static final String PROPERTY_MUTATOR_TEMPLATE = "" +
-        "'{'" +
-        "  {0} = $1; " +
-        "'}'";
-        
-    /**
-     *  The code template for the standard persistent property mutator method.    
-     *                                      <p>
-     *  Legend:                             <br>
-     *      {0} = property field name       <br>
-     *      {1} = property name             <br>
-     */
-    private static final String PERSISTENT_PROPERTY_MUTATOR_TEMPLATE =
-        "" +
-        "'{'" +
-        "  {0} = $1;" +
-        "  fireObservedChange(\"{1}\", {0}); " +
-        "'}'";
-
-    private ClassPool _classPool;
-    private CtClass _genClass;
-
-    public ClassFabricator(String className, CtClass parentClass, ClassPool classPool)
-    {
-        _classPool = classPool;
-        _genClass = _classPool.makeClass(className, parentClass);
-    }
-
-    public CtField getField(String fieldName)
-    {
-        try
-        {
-            return _genClass.getField(fieldName);
-        }
-        catch (NotFoundException e)
-        {
-            return null;
-        }
-    }
-    
-    public void createField(CtClass fieldType, String fieldName)
-    {
-        if (LOG.isDebugEnabled())
-            LOG.debug("Creating field: " + fieldName);
-
-        try
-        {
-            CtField field = new CtField(fieldType, fieldName, _genClass);
-            _genClass.addField(field);
-        }
-        catch (CannotCompileException e)
-        {
-            throw new CodeGenerationException(e);
-        }
-    }
-
-    public void createField(CtClass fieldType, String fieldName, String init)
-    {
-        if (LOG.isDebugEnabled())
-            LOG.debug("Creating field: " + fieldName + " with initializer: " + init);
-
-        try
-        {
-            CtField field = new CtField(fieldType, fieldName, _genClass);
-            _genClass.addField(field, init);
-        }
-        catch (CannotCompileException e)
-        {
-            throw new CodeGenerationException(e);
-        }
-    }
-
-    public CtMethod getMethod(String name, String signature)
-    {
-        try
-        {
-            return _genClass.getMethod(name, signature);
-        }
-        catch (NotFoundException e)
-        {
-            return null;
-        }
-    }
-
-    public void addMethod(CtMethod method) throws CannotCompileException
-    {
-        _genClass.addMethod(method);
-    }
-
-    /**
-     *  Constructs an accessor method name.
-     * 
-     **/
-
-    public String buildMethodName(String prefix, String propertyName)
-    {
-        StringBuffer result = new StringBuffer(prefix);
-
-        char ch = propertyName.charAt(0);
-
-        result.append(Character.toUpperCase(ch));
-
-        result.append(propertyName.substring(1));
-
-        return result.toString();
-    }
-
-    public CtMethod createMethod(
-        CtClass returnType,
-        String methodName,
-        CtClass[] arguments)
-    {
-        if (LOG.isDebugEnabled())
-            LOG.debug("Creating method: " + methodName);
-
-        CtMethod method = new CtMethod(returnType, methodName, arguments, _genClass);
-
-        return method;
-    }
-
-    public CtMethod createAccessor(
-        CtClass fieldType,
-        String propertyName,
-        String readMethodName)
-    {
-        String methodName =
-            readMethodName == null ? buildMethodName("get", propertyName) : readMethodName;
-
-        if (LOG.isDebugEnabled())
-            LOG.debug("Creating accessor: " + methodName);
-
-        CtMethod method = new CtMethod(fieldType, methodName, new CtClass[0], _genClass);
-
-        return method;
-    }
-
-    /**
-     *  Creates an accessor (getter) method for the property.
-     * 
-     *  @param fieldType the return type for the method
-     *  @param fieldName the name of the field (not the name of the property)
-     *  @param propertyName the name of the property (used to build the name of the method)
-     *  @param readMethodName if not null, the name of the method to use
-     * 
-     **/
-
-    public void createPropertyAccessor(
-        CtClass fieldType,
-        String fieldName,
-        String propertyName,
-        String readMethodName)
-    {
-        try
-        {
-            String accessorBody =
-                MessageFormat.format(PROPERTY_ACCESSOR_TEMPLATE, new Object[] { fieldName, propertyName });
-
-            CtMethod method = createAccessor(fieldType, propertyName, readMethodName);
-            method.setBody(accessorBody);
-            _genClass.addMethod(method);
-        }
-        catch (CannotCompileException e)
-        {
-            throw new CodeGenerationException(e);
-        }
-    }
-
-    public CtMethod createMutator(
-        CtClass fieldType,
-        String propertyName)
-    {
-        String methodName = buildMethodName("set", propertyName);
-
-        if (LOG.isDebugEnabled())
-            LOG.debug("Creating mutator: " + methodName);
-
-        CtMethod method =
-            new CtMethod(CtClass.voidType, methodName, new CtClass[] { fieldType }, _genClass);
-
-        return method;
-    }
-
-    /**
-     *  Creates a mutator (aka "setter") method.
-     * 
-     *  @param fieldType type of field value (and type of parameter value)
-     *  @param fieldName name of field (not property!)
-     *  @param propertyName name of property (used to construct method name)
-     *  @param isPersistent if true, adds a call to fireObservedChange()
-     * 
-     **/
-
-    public void createPropertyMutator(
-        CtClass fieldType,
-        String fieldName,
-        String propertyName,
-        boolean isPersistent)
-    {
-        String bodyTemplate = isPersistent ? PERSISTENT_PROPERTY_MUTATOR_TEMPLATE : PROPERTY_MUTATOR_TEMPLATE;
-        String body = MessageFormat.format(bodyTemplate, new Object[] { fieldName, propertyName });
-
-        try
-        {
-            CtMethod method = createMutator(fieldType, propertyName);
-            method.setBody(body);
-            _genClass.addMethod(method);
-        }
-        catch (CannotCompileException e)
-        {
-            throw new CodeGenerationException(e);
-        }
-    }
-
-    
-    public void commit()
-    {
-    }
-
-    public byte[] getByteCode()
-    {
-        try
-        {
-            return _genClass.toBytecode();
-        }
-        catch (NotFoundException e)
-        {
-            throw new CodeGenerationException(e);
-        }
-        catch (IOException e)
-        {
-            throw new CodeGenerationException(e);
-        }
-        catch (CannotCompileException e)
-        {
-            throw new CodeGenerationException(e);
-        }
-    }
-
-}
diff --git a/tapestry-contrib/.cvsignore b/tapestry-contrib/.cvsignore
new file mode 100644
index 0000000..602053d
--- /dev/null
+++ b/tapestry-contrib/.cvsignore
@@ -0,0 +1,3 @@
+classes

+*.log

+target

diff --git a/tapestry-contrib/build.xml b/tapestry-contrib/build.xml
new file mode 100644
index 0000000..35337d7
--- /dev/null
+++ b/tapestry-contrib/build.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<!-- $Id$ -->
+<project name="Tapestry Contrib Framework" default="install">
+	<property name="root.dir" value=".."/>
+	<property file="${root.dir}/config/Version.properties"/>
+	<property file="${root.dir}/config/build.properties"/>
+	<property file="${root.dir}/config/common.properties"/>
+	<path id="jetty.classpath">
+		<fileset dir="${jetty.dir}">
+			<include name="**/javax.*.jar"/>
+		</fileset>
+	</path>
+	<path id="project.classpath">
+		<fileset dir="${root.lib.dir}">
+			<include name="${framework.jar}"/>
+			<include name="${ext.dir}/*.jar"/>
+			<include name="${j2ee.dir}/*.jar"/>
+		</fileset>
+	</path>
+	<target name="init">
+		<mkdir dir="${classes.dir}"/>
+	</target>
+	<target name="clean">
+		<delete dir="${classes.dir}"/>
+	</target>
+
+	<target name="compile" depends="init"
+		description="Compile all classes in the framework.">
+		<javac srcdir="${src.dir}" destdir="${classes.dir}" debug="on"
+			target="1.1" source="1.3">
+			<classpath refid="project.classpath"/>
+		</javac>
+	</target>
+
+	<target name="install" depends="compile"
+		description="Compile all classes and build the installed JAR including all package resources."
+		>
+		<jar jarfile="${root.lib.dir}/${contrib.jar}">
+			<fileset dir="${classes.dir}"/>
+			<fileset dir="${src.dir}">
+				<exclude name="**/*.java"/>
+				<exclude name="**/package.html"/>
+			</fileset>
+		</jar>
+	</target>
+</project>
diff --git a/tapestry-contrib/pom.xml b/tapestry-contrib/pom.xml
new file mode 100644
index 0000000..dd5b3c3
--- /dev/null
+++ b/tapestry-contrib/pom.xml
@@ -0,0 +1,118 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.apache.tapestry</groupId>
+    <artifactId>tapestry-contrib</artifactId>
+    <packaging>jar</packaging>
+    <version>3.0.5-SNAPSHOT</version>
+
+    <parent>
+        <groupId>org.apache.tapestry</groupId>
+        <artifactId>tapestry-project</artifactId>
+        <version>3.0.5-SNAPSHOT</version>
+    </parent>
+    <name>Contrib</name>
+    <inceptionYear>2006</inceptionYear>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.tapestry</groupId>
+            <artifactId>tapestry-framework</artifactId>
+            <version>3.0.5-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>jboss</groupId>
+            <artifactId>jboss-j2ee</artifactId>
+            <version>4.0.2</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>servlet-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>log4j</groupId>
+            <artifactId>log4j</artifactId>
+            <version>1.2.9</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>commons-collections</groupId>
+            <artifactId>commons-collections</artifactId>
+            <version>2.1.1</version>
+        </dependency>
+        <dependency>
+            <groupId>ognl</groupId>
+            <artifactId>ognl</artifactId>
+            <scope>compile</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <sourceDirectory>src</sourceDirectory>
+        <resources>
+            <resource>
+                <directory>src</directory>
+                <includes>
+                    <include>**/*</include>
+                    <include>**/*.library</include>
+                    <include>**/*.page</include>
+                    <include>**/*.jwc</include>
+                </includes>
+                <excludes>
+                    <exclude>**/*.java</exclude>
+                </excludes>
+            </resource>
+        </resources>
+        <!--
+        <testSourceDirectory>src/test</testSourceDirectory>
+        <testResources>
+            <testResource>
+                <directory>src/test</directory>
+                <includes><include>**/*</include></includes>
+                 <excludes>
+                    <exclude>**/*.java</exclude>
+                </excludes>
+            </testResource>
+        </testResources>
+        -->
+
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>2.1</version>
+                <configuration>
+                    <archive>
+                        <compress>true</compress>
+                        <index>true</index>
+                    </archive>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-source-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <archive>
+                        <compress>true</compress>
+                        <index>true</index>
+                    </archive>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <reporting>
+        <outputDirectory>../target/site/tapestry-contrib</outputDirectory>
+        <excludeDefaults>true</excludeDefaults>
+    </reporting>
+
+</project>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/Contrib.library b/tapestry-contrib/src/org/apache/tapestry/contrib/Contrib.library
new file mode 100644
index 0000000..fb5b813
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/Contrib.library
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE library-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<library-specification>
+
+  <component-type type="InspectorButton" specification-path="inspector/InspectorButton.jwc"/>
+  <page name="Inspector" specification-path="inspector/Inspector.page"/>
+
+  <library id="inspector" specification-path="inspector/Inspector.library"/>
+
+  <component-type type="Choose" specification-path="components/Choose.jwc"/>
+  <component-type type="When" specification-path="components/When.jwc"/>
+  <component-type type="Otherwise" specification-path="components/Otherwise.jwc"/>
+
+  <component-type type="Palette" specification-path="palette/Palette.jwc"/>
+  <component-type type="MultiplePropertySelection" specification-path="form/MultiplePropertySelection.jwc"/>
+  <component-type type="DateField" specification-path="valid/DateField.jwc"/>
+  <component-type type="MaskEdit" specification-path="form/MaskEdit.jwc"/>
+  <component-type type="NumericField" specification-path="valid/NumericField.jwc"/>
+  <component-type type="ValidatingTextField" specification-path="valid/ValidatingTextField.jwc"/>
+  <component-type type="FormConditional" specification-path="form/FormConditional.jwc"/>
+
+  <component-type type="InheritInformalAny" specification-path="informal/InheritInformalAny.jwc"/>
+
+  <component-type type="Table" specification-path="table/components/Table.jwc"/>
+  <component-type type="TableColumns" specification-path="table/components/TableColumns.jwc"/>
+  <component-type type="TablePages" specification-path="table/components/TablePages.jwc"/>
+  <component-type type="TableRows" specification-path="table/components/TableRows.jwc"/>
+  <component-type type="TableValues" specification-path="table/components/TableValues.jwc"/>
+  <component-type type="TableView" specification-path="table/components/TableView.jwc"/>
+  <component-type type="FormTable" specification-path="table/components/FormTable.jwc"/>
+  <component-type type="TableFormRows" specification-path="table/components/TableFormRows.jwc"/>
+  <component-type type="TableFormPages" specification-path="table/components/TableFormPages.jwc"/>
+
+  <page name="SimpleTableColumnPage" specification-path="table/components/inserted/SimpleTableColumnPage.page"/>
+  <component-type type="SimpleTableColumnComponent" specification-path="table/components/inserted/SimpleTableColumnComponent.jwc"/>
+  <component-type type="SimpleTableColumnFormComponent" specification-path="table/components/inserted/SimpleTableColumnFormComponent.jwc"/>
+
+  <component-type type="PopupLink" specification-path="popup/PopupLink.jwc"/>
+
+    <component-type type="Tree"
+        specification-path="/org/apache/tapestry/contrib/tree/components/Tree.jwc"/>
+    <component-type type="TreeDataView"
+        specification-path="/org/apache/tapestry/contrib/tree/components/TreeDataView.jwc"/>
+    <component-type type="TreeNodeView"
+        specification-path="/org/apache/tapestry/contrib/tree/components/TreeNodeView.jwc"/>
+    <component-type type="TreeView"
+        specification-path="/org/apache/tapestry/contrib/tree/components/TreeView.jwc"/>
+    <component-type type="TreeTableDataView"
+        specification-path="/org/apache/tapestry/contrib/tree/components/table/TreeTableDataView.jwc"/>
+    <component-type type="TreeTable"
+        specification-path="/org/apache/tapestry/contrib/tree/components/table/TreeTable.jwc"/>
+    <component-type type="TreeTableNodeViewDelegator"
+        specification-path="/org/apache/tapestry/contrib/tree/components/table/TreeTableNodeViewDelegator.jwc"/>
+    <page name="TreeNodeViewPage" specification-path="/org/apache/tapestry/contrib/tree/components/TreeNodeViewPage.page"/>
+
+    <page name="TreeTableNodeViewPage" specification-path="/org/apache/tapestry/contrib/tree/components/table/TreeTableNodeViewPage.page"/>
+</library-specification>
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/components/Choose.java b/tapestry-contrib/src/org/apache/tapestry/contrib/components/Choose.java
new file mode 100644
index 0000000..20c41f4
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/components/Choose.java
@@ -0,0 +1,62 @@
+//  Copyright 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.tapestry.contrib.components;
+
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.components.Conditional;
+
+/**
+ *  This component is a container for {@link When} or Otherwise components;
+ *  it provides the context for mutually exclusive conditional evaluation.
+ *
+ *  [<a href="../../../../../../ComponentReference/contrib.Choose.html">Component Reference</a>]
+ *
+ *  @author David Solis
+ *  @version $Id$
+ * 
+ **/
+public abstract class Choose extends Conditional {
+
+
+	public void addBody(IRender element)
+	{
+		super.addBody(element);
+		if (element instanceof When)
+			((When) element).setChoose(this);	
+	}
+	
+	protected void cleanupAfterRender(IRequestCycle cycle)
+	{
+		setConditionMet(false);
+		super.cleanupAfterRender(cycle);
+	}
+	
+	protected boolean evaluateCondition()
+	{
+		return getCondition();
+	}
+
+	public boolean getInvert()
+	{
+		// This component doesn't require invert parameter.
+		return false;
+	}
+
+	public abstract boolean getCondition();
+	
+	public abstract boolean isConditionMet();
+	public abstract void setConditionMet(boolean value);
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/components/Choose.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/components/Choose.jwc
new file mode 100644
index 0000000..e3a522c
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/components/Choose.jwc
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.contrib.components.Choose">
+
+  <description>
+  Provides the context for mutually exclusive conditional evaluation.
+  </description>
+
+  <parameter name="condition" type="boolean" direction="in" default-value="true">
+    <description>
+    The condition to evaluate.
+    </description>
+  </parameter>
+
+  <parameter name="element" type="java.lang.String" direction="in">
+  	<description>
+  	The element to emulate.
+  	</description>
+  </parameter>
+
+  <reserved-parameter name="invert"/>
+  
+  <property-specification name="conditionMet" type="boolean" initial-value="false"/>
+  
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/components/Otherwise.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/components/Otherwise.jwc
new file mode 100644
index 0000000..3beb4f9
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/components/Otherwise.jwc
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.contrib.components.When">
+  <description>
+  Otherwise is just a When component that always tries to render its body and/or emulate an element 
+  and its attributes (if element is specified) .
+  </description>
+  
+  <parameter name="element" type="java.lang.String" direction="in">
+  	<description>
+  	The element to emulate.
+  	</description>
+  </parameter>
+
+  <reserved-parameter name="condition"/>
+
+  <reserved-parameter name="invert"/>
+
+  <property-specification name="condition" type="boolean" initial-value="true"/>
+  
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/components/When.java b/tapestry-contrib/src/org/apache/tapestry/contrib/components/When.java
new file mode 100644
index 0000000..9bcc3fa
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/components/When.java
@@ -0,0 +1,91 @@
+//  Copyright 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.tapestry.contrib.components;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.components.Conditional;
+
+/**
+ *  Represents an alternative whithin a {@link Choose} component. 
+ *  The default alternative is described by the Otherwise component.
+ *
+ *  [<a href="../../../../../../ComponentReference/contrib.When.html">Component Reference</a>]
+ *
+ *  @author David Solis
+ *  @version $Id$
+ * 
+ **/
+public abstract class When extends Conditional
+{
+    /** Parent of this component. */
+
+    private Choose _choose;
+
+    /**
+     *  Renders its wrapped components only if the condition is true and its parent {@link Choose}
+     *  allows it. In addition, if element is specified, can emulate that HTML element.
+     *
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        Choose choose = getChoose();
+
+        if (choose == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("When.must-be-contained-by-choose"),
+                this,
+                null,
+                null);
+
+        if (!choose.isConditionMet() && getCondition())
+        {
+            choose.setConditionMet(true);
+            super.renderComponent(writer, cycle);
+        }
+    }
+
+    protected boolean evaluateCondition()
+    {
+        return true;
+    }
+
+    public boolean getInvert()
+    {
+        // This component doesn't require invert parameter.
+        return false;
+    }
+
+    /**
+     *  @return Choose
+     */
+    public Choose getChoose()
+    {
+        return _choose;
+    }
+
+    /**
+     *  Sets the choose.
+     *  @param value The choose to set
+     */
+    public void setChoose(Choose value)
+    {
+        _choose = value;
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/components/When.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/components/When.jwc
new file mode 100644
index 0000000..be9b7a8
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/components/When.jwc
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.contrib.components.When">
+  <description>
+  If this is the first When component to evaluate to true within Choose then emulates an element 
+  and its attributes (if element is specified) and/or includes a block of content.
+  </description>
+  
+  <parameter name="condition" type="boolean" direction="in">
+    <description>
+    The condition to evaluate.
+    </description>
+  </parameter>
+  
+  <parameter name="element" type="java.lang.String" direction="in">
+  	<description>
+  	The element to emulate.
+  	</description>
+  </parameter>
+
+  <reserved-parameter name="invert"/>
+  
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/components/package.html b/tapestry-contrib/src/org/apache/tapestry/contrib/components/package.html
new file mode 100644
index 0000000..14e9648
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/components/package.html
@@ -0,0 +1,14 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+<p>Contribution of foundational components.
+
+@author David Solis <a href="mailto:dsolis@apache.org">dsolis@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/ejb/XCreateException.java b/tapestry-contrib/src/org/apache/tapestry/contrib/ejb/XCreateException.java
new file mode 100644
index 0000000..1e8bb86
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/ejb/XCreateException.java
@@ -0,0 +1,54 @@
+//  Copyright 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.tapestry.contrib.ejb;
+
+import javax.ejb.CreateException;
+
+/**
+ *  Extended version of {@link CreateException} that includes a root cause.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public class XCreateException extends CreateException
+{
+    private Throwable rootCause;
+
+    public XCreateException(String message)
+    {
+        super(message);
+    }
+
+    public XCreateException(String message, Throwable rootCause)
+    {
+        super(message);
+
+        this.rootCause = rootCause;
+    }
+
+    public XCreateException(Throwable rootCause)
+    {
+        super(rootCause.getMessage());
+
+        this.rootCause = rootCause;
+    }
+
+    public Throwable getRootCause()
+    {
+        return rootCause;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/ejb/XEJBException.java b/tapestry-contrib/src/org/apache/tapestry/contrib/ejb/XEJBException.java
new file mode 100644
index 0000000..bd92f4c
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/ejb/XEJBException.java
@@ -0,0 +1,57 @@
+//  Copyright 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.tapestry.contrib.ejb;
+
+import javax.ejb.EJBException;
+
+/**
+ *  Extended version of {@link EJBException} that includes a root cause.
+ *  {@link EJBException} doesn't have quite the right constructor for this ...
+ *  it has an equivalent to the rootCause property, (causedByException), but
+ *  doesn't have a constructor that allows us to set a custom message.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public class XEJBException extends EJBException
+{
+    private Throwable rootCause;
+
+    public XEJBException(String message)
+    {
+        super(message);
+    }
+
+    public XEJBException(String message, Throwable rootCause)
+    {
+        super(message);
+
+        this.rootCause = rootCause;
+    }
+
+    public XEJBException(Throwable rootCause)
+    {
+        super(rootCause.getMessage());
+
+        this.rootCause = rootCause;
+    }
+
+    public Throwable getRootCause()
+    {
+        return rootCause;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/ejb/XFinderException.java b/tapestry-contrib/src/org/apache/tapestry/contrib/ejb/XFinderException.java
new file mode 100644
index 0000000..a340ee8
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/ejb/XFinderException.java
@@ -0,0 +1,54 @@
+//  Copyright 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.tapestry.contrib.ejb;
+
+import javax.ejb.FinderException;
+
+/**
+ *  Extended version of {@link FinderException} that includes a root cause.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public class XFinderException extends FinderException
+{
+    private Throwable rootCause;
+
+    public XFinderException(String message)
+    {
+        super(message);
+    }
+
+    public XFinderException(String message, Throwable rootCause)
+    {
+        super(message);
+
+        this.rootCause = rootCause;
+    }
+
+    public XFinderException(Throwable rootCause)
+    {
+        super(rootCause.getMessage());
+
+        this.rootCause = rootCause;
+    }
+
+    public Throwable getRootCause()
+    {
+        return rootCause;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/ejb/XRemoveException.java b/tapestry-contrib/src/org/apache/tapestry/contrib/ejb/XRemoveException.java
new file mode 100644
index 0000000..92b0132
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/ejb/XRemoveException.java
@@ -0,0 +1,54 @@
+//  Copyright 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.tapestry.contrib.ejb;
+
+import javax.ejb.RemoveException;
+
+/**
+ *  Extended version of {@link RemoveException} that includes a root cause.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public class XRemoveException extends RemoveException
+{
+    private Throwable rootCause;
+
+    public XRemoveException(String message)
+    {
+        super(message);
+    }
+
+    public XRemoveException(String message, Throwable rootCause)
+    {
+        super(message);
+
+        this.rootCause = rootCause;
+    }
+
+    public XRemoveException(Throwable rootCause)
+    {
+        super(rootCause.getMessage());
+
+        this.rootCause = rootCause;
+    }
+
+    public Throwable getRootCause()
+    {
+        return rootCause;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/ejb/package.html b/tapestry-contrib/src/org/apache/tapestry/contrib/ejb/package.html
new file mode 100644
index 0000000..1e0fbfb
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/ejb/package.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+<p>Subclasses of the EJB exceptions that take an optional, additional, root cause Throwable
+to identify the underlying reason for the exception.  This is less necessary in JDK 1.4, which
+support root cause for all exceptions.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/form/CheckBoxMultiplePropertySelectionRenderer.java b/tapestry-contrib/src/org/apache/tapestry/contrib/form/CheckBoxMultiplePropertySelectionRenderer.java
new file mode 100644
index 0000000..3661094
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/form/CheckBoxMultiplePropertySelectionRenderer.java
@@ -0,0 +1,103 @@
+//  Copyright 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.tapestry.contrib.form;
+
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.form.IPropertySelectionModel;
+
+/**
+ *  Implementation of {@link IMultiplePropertySelectionRenderer} that
+ *  produces a table of checkbox (&lt;input type=checkbox&gt;) elements.
+ *
+ *  @version $Id$
+ *  @author Sanjay Munjal
+ *
+ **/
+
+public class CheckBoxMultiplePropertySelectionRenderer
+    implements IMultiplePropertySelectionRenderer
+{
+
+    /**
+     *  Writes the &lt;table&gt; element.
+     *
+     **/
+
+    public void beginRender(
+        MultiplePropertySelection component,
+        IMarkupWriter writer,
+        IRequestCycle cycle)
+    {
+        writer.begin("table");
+        writer.attribute("border", 0);
+        writer.attribute("cellpadding", 0);
+        writer.attribute("cellspacing", 2);
+    }
+
+    /**
+     *  Closes the &lt;table&gt; element.
+     *
+     **/
+
+    public void endRender(
+        MultiplePropertySelection component,
+        IMarkupWriter writer,
+        IRequestCycle cycle)
+    {
+        writer.end(); // <table>
+    }
+
+    /**
+     *  Writes a row of the table.  The table contains two cells; the first is the checkbox,
+     *  the second is the label for the checkbox.
+     *
+     **/
+
+    public void renderOption(
+        MultiplePropertySelection component,
+        IMarkupWriter writer,
+        IRequestCycle cycle,
+        IPropertySelectionModel model,
+        Object option,
+        int index,
+        boolean selected)
+    {
+        writer.begin("tr");
+        writer.begin("td");
+
+        writer.beginEmpty("input");
+        writer.attribute("type", "checkbox");
+        writer.attribute("name", component.getName());
+        writer.attribute("value", model.getValue(index));
+
+        if (component.isDisabled())
+            writer.attribute("disabled", "disabled");
+
+        if (selected)
+            writer.attribute("checked", "checked");
+
+        writer.end(); // <td>
+
+        writer.println();
+
+        writer.begin("td");
+        writer.print(model.getLabel(index));
+        writer.end(); // <td>
+        writer.end(); // <tr>
+
+        writer.println();
+    }
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/form/FormConditional.java b/tapestry-contrib/src/org/apache/tapestry/contrib/form/FormConditional.java
new file mode 100644
index 0000000..c76fad0
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/form/FormConditional.java
@@ -0,0 +1,168 @@
+//  Copyright 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.tapestry.contrib.form;
+
+import java.io.IOException;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IActionListener;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.form.AbstractFormComponent;
+import org.apache.tapestry.request.RequestContext;
+import org.apache.tapestry.util.io.DataSqueezer;
+
+/**
+ *  A conditional element on a page which will render its wrapped elements
+ *  zero or one times.
+ * 
+ * This component is a variant of {@link org.apache.tapestry.components.Conditional}, 
+ * but is designed for operation in a form. The component parameters are stored in 
+ * hidden fields during rendering and are taken from those fields during the rewind, 
+ * thus no StaleLink exceptions occur. 
+ *
+ *  [<a href="../../../../../ComponentReference/contrib.FormConditional.html">Component Reference</a>]
+ *
+ *  @author Mindbridge
+ *  @version $Id$
+ *  @since 3.0
+ * 
+ **/
+
+public abstract class FormConditional extends AbstractFormComponent
+{
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        IForm form = getForm(cycle);
+
+        boolean cycleRewinding = cycle.isRewinding();
+
+        // If the cycle is rewinding, but not this particular form,
+        // then do nothing (don't even render the body).
+
+        if (cycleRewinding && !form.isRewinding())
+            return;
+
+        String name = form.getElementId(this);
+
+        boolean condition = getCondition(cycle, form, name);
+
+        // call listener
+        IActionListener listener = getListener();
+        if (listener != null)
+            listener.actionTriggered(this, cycle);
+
+        // render the component body only if the condition is true
+        if (condition) {
+            String element = getElement();
+            
+            boolean render = !cycleRewinding && Tapestry.isNonBlank(element);
+            
+            if (render)
+            {
+                writer.begin(element);
+                renderInformalParameters(writer, cycle);
+            }
+
+            renderBody(writer, cycle);
+            
+            if (render)
+                writer.end(element);
+        }
+    }
+
+    private boolean getCondition(IRequestCycle cycle, IForm form, String name)
+    {
+        boolean condition;
+        
+        if (!cycle.isRewinding())
+        {
+            condition = getCondition();
+            writeValue(form, name, condition);
+        }
+        else
+        {
+            RequestContext context = cycle.getRequestContext();
+            String submittedConditions[] = context.getParameters(name);
+            condition = convertValue(submittedConditions[0]);
+        }
+
+        IBinding conditionValueBinding = getConditionValueBinding();
+        if  (conditionValueBinding != null) 
+            conditionValueBinding.setBoolean(condition);
+        
+        return condition;
+    }
+
+    private void writeValue(IForm form, String name, boolean value)
+    {
+        String externalValue;
+
+        Object booleanValue = new Boolean(value);
+        try
+        {
+            externalValue = getDataSqueezer().squeeze(booleanValue);
+        }
+        catch (IOException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("FormConditional.unable-to-convert-value", booleanValue),
+                this,
+                null,
+                ex);
+        }
+
+        form.addHiddenValue(name, externalValue);
+    }
+
+    private boolean convertValue(String value)
+    {
+        try
+        {
+            Object booleanValue = getDataSqueezer().unsqueeze(value);
+            return Tapestry.evaluateBoolean(booleanValue);
+        }
+        catch (IOException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("FormConditional.unable-to-convert-string", value),
+                this,
+                null,
+                ex);
+        }
+    }
+
+    private DataSqueezer getDataSqueezer()
+    {
+        return getPage().getEngine().getDataSqueezer();
+    }
+
+    public boolean isDisabled()
+    {
+        return false;
+    }
+
+    public abstract boolean getCondition();
+    public abstract String getElement();
+
+    public abstract IBinding getConditionValueBinding();
+
+    public abstract IActionListener getListener();
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/form/FormConditional.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/form/FormConditional.jwc
new file mode 100644
index 0000000..c2ba3d3
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/form/FormConditional.jwc
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.contrib.form.FormConditional">
+  <description>
+  Conditionally emulates an element and its attributes (if element is specified) and/or includes a block of content if a condition is met.
+  </description>
+  
+  <parameter name="condition" type="boolean" direction="in" required="yes">
+    <description>
+    The condition to evaluate.
+    </description>
+  </parameter>
+  
+  <parameter name="element" type="java.lang.String" direction="in" required="no">
+  	<description>
+  	The element to emulate.
+  	</description>
+  </parameter>
+
+  <parameter name="listener" type="org.apache.tapestry.IActionListener" direction="in"/>
+
+  <parameter name="conditionValue" type="boolean">
+    <description>
+    The value of the condition. During render this is obtained from
+    the condition parameter. During rewind it is the submitted condition.
+    </description>
+  </parameter>
+
+  <reserved-parameter name="invert"/>
+  
+  <property-specification name="name" type="java.lang.String"/>
+  <property-specification name="form" type="org.apache.tapestry.IForm"/>
+  
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/form/IMultiplePropertySelectionRenderer.java b/tapestry-contrib/src/org/apache/tapestry/contrib/form/IMultiplePropertySelectionRenderer.java
new file mode 100644
index 0000000..a70af59
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/form/IMultiplePropertySelectionRenderer.java
@@ -0,0 +1,65 @@
+//  Copyright 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.tapestry.contrib.form;
+
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.form.IPropertySelectionModel;
+
+/**
+ *  Defines an object that works with a {@link MultiplePropertySelection} component
+ *  to render the individual elements obtained from the {@link IPropertySelectionModel model}.
+ *
+ *  @version $Id$
+ *  @author Sanjay Munjal
+ *
+ **/
+
+public interface IMultiplePropertySelectionRenderer
+{
+    /**
+     *  Begins the rendering of the {@link MultiplePropertySelection}.
+     *
+     **/
+
+    public void beginRender(
+        MultiplePropertySelection component,
+        IMarkupWriter writer,
+        IRequestCycle cycle);
+
+    /**
+     *  Invoked for each element obtained from the {@link IPropertySelectionModel model}.
+     *
+     **/
+
+    public void renderOption(
+        MultiplePropertySelection component,
+        IMarkupWriter writer,
+        IRequestCycle cycle,
+        IPropertySelectionModel model,
+        Object option,
+        int index,
+        boolean selected);
+
+    /**
+     *  Ends the rendering of the {@link MultiplePropertySelection}.
+     *
+     **/
+
+    public void endRender(
+        MultiplePropertySelection component,
+        IMarkupWriter writer,
+        IRequestCycle cycle);
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/form/MaskEdit.html b/tapestry-contrib/src/org/apache/tapestry/contrib/form/MaskEdit.html
new file mode 100644
index 0000000..3e4b0a5
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/form/MaskEdit.html
@@ -0,0 +1,5 @@
+<input jwcid="maskEdit" type="text"/>
+<input jwcid="maskValue" type="hidden"/>
+<span jwcid="maskEditScript"/>
+
+
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/form/MaskEdit.java b/tapestry-contrib/src/org/apache/tapestry/contrib/form/MaskEdit.java
new file mode 100644
index 0000000..7d596b4
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/form/MaskEdit.java
@@ -0,0 +1,113 @@
+//  Copyright 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.tapestry.contrib.form;
+
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.IBinding;
+
+/**
+ * Provides a mask edit HTML &lt;input type="text"&gt; form element.
+ * <p>
+ * Mask edit field validates the text the user enters against a 
+ * mask that encodes the valid forms the text can take. The mask can 
+ * also format text that is displayed to the user.
+ * <p>
+ * <table border="1" cellpadding="2">
+ *  <tr>
+ *   <th>Mask character</th><th>Meaning in mask</th>
+ *  </tr>
+ *  <tr>
+ *   <td>&nbsp;l</td><td>&nbsp;Mixed case letter character [a..z, A..Z]</td>
+ *  </tr>
+ *  <tr>
+ *   <td>&nbsp;L</td><td>&nbsp;Upper case letter character [A..Z]</td>
+ *  </tr>
+ *  <tr>
+ *   <td>&nbsp;a</td><td>&nbsp;Mixed case alpha numeric character [a..z, A..Z, 0..1]</td>
+ *  </tr>
+ *  <tr>
+ *   <td>&nbsp;A</td><td>&nbsp;Upper case alpha numeric character [A..Z, 0..9]</td>
+ *  </tr>
+ *  <tr>
+ *   <td>&nbsp;#</td><td>&nbsp;Numeric character [0..9]</td>
+ *  </tr>
+ *  <tr>
+ *   <td>&nbsp;_</td><td>&nbsp;Reserved character for display, do not use.</td>
+ *  </tr>
+ *  <tr>
+ *   <td>&nbsp;others</td><td>&nbsp;Non editable character for display.</td>
+ *  </tr>
+ * </table> 
+ * <p>
+ * This component requires JavaScript to be enabled in the client browser.
+ * <p>
+ * [<a href="../../../../../ComponentReference/MaskEdit.html">Component Reference</a>]
+ *
+ * @author Malcolm Edgar
+ * @version $Id$
+ * @since 2.3
+ *
+ **/
+
+public class MaskEdit extends BaseComponent
+{
+    private String _mask;
+    private IBinding _valueBinding;
+    private boolean _disabled;
+
+    public String getMask()
+    {
+        return _mask;
+    }
+
+    public void setMask(String mask)
+    {
+        _mask = mask;
+    }
+    
+    public String getValue()
+    {
+        if (_valueBinding != null) {
+            return _valueBinding.getString();
+        } else {
+            return null;
+        }
+    }
+
+    public void setValue(String value)
+    {
+        _valueBinding.setString(value);
+    }
+
+    public IBinding getValueBinding()
+    {
+        return _valueBinding;
+    }
+
+    public void setValueBinding(IBinding valueBinding)
+    {
+        _valueBinding = valueBinding;
+    }
+
+    public boolean isDisabled()
+    {
+        return _disabled;
+    }
+
+    public void setDisabled(boolean disabled)
+    {
+        _disabled = disabled;
+    }        
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/form/MaskEdit.js b/tapestry-contrib/src/org/apache/tapestry/contrib/form/MaskEdit.js
new file mode 100644
index 0000000..9db04c8
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/form/MaskEdit.js
@@ -0,0 +1,420 @@
+/**

+ *  JavaScript Mask Edit control

+ *  Paul Geerts

+ *  Oct 2002

+ * 

+ *  Note:  This probably only works for English

+ *  Other languages have been deprecated and will be removed in the 

+ *  next version of Speech (TM)

+ **/

+

+var dontDoIt;  // hack for Moz because it won't cancel events properly

+var isTab;     // another Moz hack

+

+

+// Init the mask edit field by creating a lookalike DIV

+// and hiding the real one

+function initMask(field, maskField) {

+

+	if (field.disabled == true) {

+	   return;

+	}

+

+    var mask = maskField.value;

+    var val = field.value;

+    

+    if (!val) {  // if there's no val, init it with empty mask

+        val = displayMask(mask);

+        field.value = displayMask(mask);

+    }

+    // create a div and add a bunch of spans

+    // and edits to it.

+    div = document.createElement("div");

+   	div.style.backgroundColor = "white";

+    for (var i = 0 ; i < mask.length ; i++) {

+        var ds = document.createElement("SPAN");

+        var v = val.substr(i,1);

+        var m = mask.substr(i,1);

+        if (v==" ") {

+            v="&nbsp;";

+        }

+        ds.innerHTML = v;

+        ds.index = i;

+        ds.mask = m;

+        ds.div = div;

+        // if we can edit this char

+        // make a little tiny edit field

+        if (isEditChar(m)) {

+            var es = document.createElement("INPUT");

+            es.style.width = "1px";

+            es.style.border="0px";

+            es.index = i;

+            es.field = field;

+            es.mask = m;

+            es.display = ds;

+            ds.editField = es;

+            es.div = div;

+            div.appendChild(es);  // set up some events

+            if (navigator.appName == "Microsoft Internet Explorer") {

+                addEvent("keypress", es, changeBitIE);

+            } else {

+                addEvent("keypress", es, changeBitNS);

+            }

+            addEvent("keydown", es, specialKey); // keydown handles stuff like home, end etc

+            addEvent("click", ds, click);

+        } 

+

+        div.appendChild(ds);

+

+    }

+    

+    // the final edit field on the end

+    var es =document.createElement("INPUT");

+    es.style.width = "1px";

+    es.style.border="0px";

+    es.div = div;

+    div.appendChild(es);

+    if (navigator.appName == "Microsoft Internet Explorer") {

+        addEvent("keypress", es, changeBitIE);

+    } else {

+        addEvent("keypress", es, changeBitNS);

+    }

+    addEvent("keydown", es, specialKey);

+

+    div.noWrap = true; // force single line display

+

+    formatDiv(div, field); // format the DIV to look like an edit box

+    field.style.display = 'none'; 

+    field.parentNode.insertBefore(div, field);

+    addEvent("click", div, divClick);

+}

+

+function formatDiv(div, field) {

+    // make it look like an IE edit

+    if (navigator.appName == "Microsoft Internet Explorer") {

+        div.style.fontFamily="courier"; 

+        div.style.fontSize="10pt";      

+        div.style.width = field.offsetWidth;

+        div.style.height = field.offsetHeight;

+        if (navigator.appVersion.match(/6.0/)) { // IE 6 is different

+            div.style.border = "1px solid #7F9DB9";

+        } else {

+            div.style.borderLeft = "2px solid #606060";

+            div.style.borderTop = "2px solid #606060";

+            div.style.borderRight = "1px solid #aaaaaa";

+            div.style.borderBottom = "1px solid #aaaaaa";

+        }

+

+    } else {

+        // Mozilla edit look-a-like

+        div.style.fontFamily="courier";

+        div.style.fontSize="10pt";

+        div.style.border="2px inset #cccccc";

+        if (field.size) {

+            div.style.widh = 13 * field.size;

+        } else {

+            div.style.width = "130px";

+        }

+    }

+}

+

+

+function isEditChar(c) {  // is this char a meaningful mask char

+    switch (c) {

+    case "_":

+    case "#":

+    case "a":

+    case "A":

+    case "l":

+    case "L":

+        return true;

+    default:

+        return false;

+    }

+    return false;

+}

+

+function displayMaskChar(c) {  // display mask chars as _ 

+    if (isEditChar(c)) {       // otherwise just show normal char

+        return "_";

+    } else {

+        return c;

+    }

+}

+

+function displayMask(mask) {  // display entire mask using about subroutine

+    var d = "";

+    for (var i = 0 ; i < mask.length ; i++) {

+        d+=displayMaskChar(mask.substr(i,1));

+    }

+    return d;

+}

+

+function divClick(e) {     // when the main DIV is clicked, focus the end of the edit

+    var d = getEventObject(e);

+    if (d && d.lastChild) {

+        try {

+           d.lastChild.focus();

+        } catch (e) {

+           // nuffin

+        }

+    }

+}

+

+function specialKey(e) { // deal with special keys like backspace, delete etc

+    var s = getEventObject(e);

+    var code = e.keyCode;

+    dontDoIt = true;  // Moz needs these, as I can't seem to cancel events properly

+    isTab = false;    // Moz can't handle tabs well either

+    switch (code) {

+    case 8:   // backspace

+        var b = getPrevEdit(s);

+        if (b) {

+            b.display.innerHTML = displayMaskChar(b.mask);

+            var i = b.index;

+            b.field.value = b.field.value.substr(0, i) + 

+                displayMaskChar(b.mask) + b.field.value.substr(i+1, b.field.value.length - i);

+            b.focus();

+        }

+        cancelEvent(e);

+        return false;

+    case 46:  // delete

+        if (s.display) {

+            s.display.innerHTML = displayMaskChar(s.mask);

+            var i = s.index;

+            s.field.value = s.field.value.substr(0, i) + displayMaskChar(s.mask) + 

+                s.field.value.substr(i+1, s.field.value.length - i);

+        }

+        cancelEvent(e);

+        return false;

+        break;

+    case 37: // left

+        var p = getPrevEdit(s);

+        if (p) {

+            p.focus();

+        }

+        cancelEvent(e);

+        return false;

+    case 39: // right

+        var n = getNextEdit(s);

+        if (n) {

+            n.focus();

+        }

+        cancelEvent(e);

+        return false;

+    case 36: // home

+        s.div.firstChild.focus();

+        cancelEvent(e);

+        return false;

+    case 35: // end

+        s.div.lastChild.focus();

+        cancelEvent(e);

+        return false;

+    case 9: // tab

+        if (navigator.appName == "Microsoft Internet Explorer") {

+            if (!e.shiftKey) {

+                s.div.lastChild.focus();

+            } else {

+                s.div.firstChild.focus();

+            }

+            return;

+        } else {  // is mozilla/netscape

+            isTab = true;  // best i can do really

+        }

+        break;

+    }

+       

+    dontDoIt = false;

+}

+

+function moveForward(s) { // focus next edit

+    var b = getNextEdit(s);

+    if (b) {

+        b.focus();

+    }

+}

+

+function moveBackward(s) { // focus previous edit

+    var b = getPrevEdit(s);

+    if (b) {

+        b.focus();

+    }

+}

+

+function isInsertOK(code, s) {  // check if you're good to insert a char

+    var mchar = s.mask;

+    switch (mchar) {

+    case "_":

+        return true;

+        break;

+    case "#":

+        return checkDigit(code);

+        break;

+    case "a":

+        return checkAlphaNumeric(code);

+        break;

+    case "A":

+        return checkUpCaseAlphaNumeric(code);

+        break;

+    case "l":

+        return checkAlpha(code);

+        break;

+    case "L":

+        return checkUpCaseAlpha(code);

+        break;

+    }

+    return false;

+}

+

+// functions to check the key code, good ol ASCII

+// fairly straightforward

+

+function checkDigit(code) {

+    if ((code>=48) && (code<=57)) {

+        return code;

+    } else {

+        return null;

+    }

+}

+

+function checkAlpha(code) {

+    if (((code>=65) && (code<=90)) || ((code>=97) && (code<=122))) {

+        return code;

+    } else {

+        return null;

+    }

+}

+

+function checkUpCaseAlpha(code) {

+    if ((code>=65) && (code<=90)) {

+        return code;

+    } else if ((code>=97) && (code<=122)) {

+        return code - 32;

+    } else {

+        return null;

+    }

+}

+

+function checkAlphaNumeric(code) {

+    if (((code>=65) && (code<=90)) || ((code>=97) && (code<=122)) || ((code>=48) && (code<=57))) {

+        return code;

+    } else {

+        return null;

+    }

+}

+

+function checkUpCaseAlphaNumeric(code) {

+    if ((code>=65) && (code<=90)) {

+        return code;

+    } else if ((code>=97) && (code<=122)) {

+        return code - 32;

+    } else if ((code>=48) && (code<=57)) {

+        return code;

+    } else {

+        return null;

+    }

+}

+

+

+function changeBitNS(e) {  // handle key events in NS

+    var es = getEventObject(e);

+    if (!isTab) {

+        if (es.display) {

+            if (!dontDoIt) {

+                var code = e.charCode;

+                if (code = isInsertOK(code, es)) {

+                    var  c = String.fromCharCode(code);

+                    es.display.innerHTML = c

+                        var i = es.index;

+                    es.field.value = es.field.value.substr(0, i) + c + es.field.value.substr(i+1, es.field.value.length - i);

+                    moveForward(es);

+                }

+            }

+            es.value = "";

+            cancelEvent(e);

+        }        

+        return false;

+    }  

+}

+

+function changeBitIE(e) { // handle key events in IE

+    var es = getEventObject(e);

+    if (es.display) {

+        var code = e.keyCode;

+        if (code = isInsertOK(code, es)) {

+            var  c = String.fromCharCode(code);

+            es.display.innerHTML = c;

+            var i = es.index;

+            es.field.value = es.field.value.substr(0, i) + c + es.field.value.substr(i+1, es.field.value.length - i);

+            moveForward(es);

+            es.value = "";

+        }

+    }

+    cancelEvent(e);

+    return false;

+}

+

+function click(e) {  // clicking on a display span focuses the edit

+    var s = getEventObject(e);

+    s.editField.focus();

+    cancelEvent(e);

+    return false;

+}

+

+function getPrevEdit(s) {    // get previous input field 

+    var b = s.previousSibling;

+    while (b && (b.tagName!="INPUT")) {

+        b = b.previousSibling;

+    }

+    return b;

+}

+

+function getNextEdit(s) { // get previous next field 

+    var b = s.nextSibling;

+    while (b && (b.tagName!="INPUT")) {

+        b = b.nextSibling;

+    }

+    return b;

+}

+

+function cancelEvent(e) {   // kill event propagation

+    e.cancelBubble = true;

+    e.cancel = true;

+    if (navigator.appName != "Microsoft Internet Explorer") {

+        e.stopPropagation();  // doesn't seem to work for key events

+        e.preventDefault();

+    }

+}

+

+

+function getEventObject(e) {  // utility function to retrieve object from event

+    if (navigator.appName == "Microsoft Internet Explorer") {

+        return e.srcElement;

+    } else {  // is mozilla/netscape

+        // need to crawl up the tree to get the first "real" element

+        // i.e. a tag, not raw text

+        var o = e.target;

+        while (!o.tagName) {

+            o = o.parentNode;

+        }

+        return o;

+    }

+}

+

+function addEvent(name, obj, funct) { // utility function to add event handlers

+

+    if (navigator.appName == "Microsoft Internet Explorer") {

+        obj.attachEvent("on"+name, funct);

+    } else {  // is mozilla/netscape

+        obj.addEventListener(name, funct, false);

+    }

+}

+

+function deleteEvent(name, obj, funct) { // utility function to delete event handlers

+

+    if (navigator.appName == "Microsoft Internet Explorer") {

+        obj.detachEvent("on"+name, funct);

+    } else {  // is mozilla/netscape

+        obj.removeEventListener(name, funct, false);

+    }

+}

diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/form/MaskEdit.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/form/MaskEdit.jwc
new file mode 100644
index 0000000..a796fcb
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/form/MaskEdit.jwc
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.contrib.form.MaskEdit" allow-informal-parameters="no">
+
+  <parameter name="mask" direction="in" type="java.lang.String" required="yes"/>
+  <parameter name="value" direction="custom" type="java.lang.String" required="yes"/>
+  <parameter name="disabled" direction="in" type="boolean" required="no"/>
+  
+  <component id="maskEdit" type="TextField">
+    <binding name="value" expression="value"/>
+    <binding name="maxlength" expression="mask.length()"/>
+    <binding name="size" expression="mask.length()"/>        
+    <binding name="disabled" expression="disabled"/>
+  </component>
+
+  <component id="maskValue" type="Hidden">
+    <binding name="value" expression="mask"/>
+    <binding name="encode" expression="false"/>
+  </component>
+
+  <component id="maskEditScript" type="Script">
+    <binding name="maskEdit" expression="components.maskEdit"/>
+    <binding name="maskValue" expression="components.maskValue"/>
+    <binding name="script" expression='"MaskEdit.script"'/>
+  </component>
+        
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/form/MaskEdit.script b/tapestry-contrib/src/org/apache/tapestry/contrib/form/MaskEdit.script
new file mode 100644
index 0000000..e6c7756
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/form/MaskEdit.script
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- $Id$ -->
+<!DOCTYPE script PUBLIC
+	"-//Howard Lewis Ship//Tapestry Script 1.2//EN"
+	"http://tapestry.sf.net/dtd/Script_1_2.dtd">
+  
+<script>
+
+<include-script resource-path="/org/apache/tapestry/contrib/form/MaskEdit.js"/>
+
+<input-symbol key="maskEdit" class="org.apache.tapestry.form.TextField" required="yes"/>
+<input-symbol key="maskValue" class="org.apache.tapestry.form.Hidden" required="yes"/>
+
+<let key="formName">
+  ${maskEdit.form.name}
+</let>
+
+<let key="functionName">
+  ${maskEdit.name}_init
+</let>
+
+
+<body>
+function ${functionName}() {
+  initMask(document.${formName}.${maskEdit.name},
+           document.${formName}.${maskValue.name});
+}
+</body>
+
+<initialization>
+  ${functionName}();
+</initialization>
+
+</script>
+
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/form/MultiplePropertySelection.java b/tapestry-contrib/src/org/apache/tapestry/contrib/form/MultiplePropertySelection.java
new file mode 100644
index 0000000..3a412d3
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/form/MultiplePropertySelection.java
@@ -0,0 +1,216 @@
+//  Copyright 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.tapestry.contrib.form;
+
+import java.util.List;
+
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.form.AbstractFormComponent;
+import org.apache.tapestry.form.IPropertySelectionModel;
+
+/**
+ *  A component which uses &lt;input type=checkbox&gt; to
+ *  set a property of some object.  Typically, the values for the object
+ *  are defined using an {@link org.apache.commons.lang.enum.Enum}.  A MultiplePropertySelection is dependent on
+ *  an {link IPropertySelectionModel} to provide the list of possible values.
+ *
+ *  <p>Often, this is used to select one or more {@link org.apache.commons.lang.enum.Enum} to assign to a property; the
+ * {@link org.apache.tapestry.form.EnumPropertySelectionModel} class simplifies this.
+ * 
+ *  <p>The {@link org.apache.tapestry.contrib.palette.Palette} component
+ *  is more powerful, but requires client-side JavaScript and 
+ *  is not fully cross-browser compatible.
+ *
+ *  <p>
+ *
+ * <table border=1>
+ * <tr>
+ *    <td>Parameter</td>
+ *    <td>Type</td>
+ *	  <td>Direction</td>
+ *    <td>Required</td>
+ *    <td>Default</td>
+ *    <td>Description</td>
+ * </tr>
+ *
+ * <tr>
+ *		<td>selectedList</td>
+ *		<td>java.util.List</td>
+ *		<td>in-out</td>
+ *		<td>yes</td>
+ *		<td>&nbsp;</td>
+ *		<td>The property to set.  During rendering, this property is read, and sets
+ * the default value of the options in the select.
+ * When the form is submitted, list is cleared, then has each
+ * selected option added to it. </td> </tr>
+ *
+ * <tr>
+ *		<td>renderer</td>
+ *		<td>{@link IMultiplePropertySelectionRenderer}</td>
+ *		<td>in</td>
+ *		<td>no</td>
+ *		<td>shared instance of {@link CheckBoxMultiplePropertySelectionRenderer}</td>
+ *		<td>Defines the object used to render this component.  The default
+ *  renders a table of checkboxes.</td></tr>
+ *
+ *  <tr>
+ *		<td>model</td>
+ *		<td>{@link IPropertySelectionModel}</td>
+ *		<td>in</td>
+ *		<td>yes</td>
+ *		<td>&nbsp;</td>
+ *		<td>The model provides a list of possible labels, and matches those labels
+ *  against possible values that can be assigned back to the property.</td> </tr>
+ *
+ *  <tr>
+ * 		<td>disabled</td>
+ *		<td>boolean</td>
+ *		<td>in</td>
+ *		<td>no</td>
+ *		<td>false</td>
+ *		<td>Controls whether the &lt;select&gt; is active or not. A disabled PropertySelection
+ * does not update its value parameter.
+ *
+ *			<p>Corresponds to the <code>disabled</code> HTML attribute.</td>
+ *	</tr>
+ *
+ *	</table>
+ *
+ * <p>Informal parameters are not allowed.
+ *
+ *
+ *  @version $Id$
+ *  @author Sanjay Munjal
+ *
+ **/
+
+public abstract class MultiplePropertySelection extends AbstractFormComponent
+{
+
+    /**
+     *  A shared instance of {@link CheckBoxMultiplePropertySelectionRenderer}.
+     *
+     **/
+
+    public static final IMultiplePropertySelectionRenderer DEFAULT_CHECKBOX_RENDERER =
+        new CheckBoxMultiplePropertySelectionRenderer();
+
+    public abstract IBinding getSelectedListBinding();
+
+    protected void finishLoad()
+    {
+        setRenderer(DEFAULT_CHECKBOX_RENDERER);
+    }
+
+    /**
+     *  Returns true if the component is disabled (this is relevant to the
+     *  renderer).
+     *
+     **/
+
+    public abstract boolean isDisabled();
+
+    /**
+     *  Renders the component, much of which is the responsiblity
+     *  of the {@link IMultiplePropertySelectionRenderer renderer}.  The possible options,
+     *  their labels, and the values to be encoded in the form are provided
+     *  by the {@link IPropertySelectionModel model}.
+     *
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        IForm form = getForm(cycle);
+
+        boolean rewinding = form.isRewinding();
+
+        String name = form.getElementId(this);
+
+        List selectedList = (List) getSelectedListBinding().getObject("selectedList", List.class);
+
+        if (selectedList == null)
+            throw Tapestry.createRequiredParameterException(this, "selectedList");
+
+        IPropertySelectionModel model = getModel();
+
+        if (model == null)
+            throw Tapestry.createRequiredParameterException(this, "model");
+
+        // Handle the form processing first.
+        if (rewinding)
+        {
+            // If disabled, ignore anything that comes up from the client.
+
+            if (isDisabled())
+                return;
+
+            // get all the values
+            String[] optionValues = cycle.getRequestContext().getParameters(name);
+
+            // Clear the list
+
+            selectedList.clear();
+
+            // Nothing was selected
+            if (optionValues != null)
+            {
+
+                // Go through the array and translate and put back in the list
+                for (int i = 0; i < optionValues.length; i++)
+                {
+                    // Translate the new value
+                    Object selectedValue = model.translateValue(optionValues[i]);
+
+                    // Add this element in the list back
+                    selectedList.add(selectedValue);
+                }
+            }
+
+            return;
+        }
+
+        IMultiplePropertySelectionRenderer renderer = getRenderer();
+
+        // Start rendering
+        renderer.beginRender(this, writer, cycle);
+
+        int count = model.getOptionCount();
+
+        for (int i = 0; i < count; i++)
+        {
+            Object option = model.getOption(i);
+
+            // Try to find the option in the list and if yes, then it is checked.
+            boolean optionSelected = selectedList.contains(option);
+
+            renderer.renderOption(this, writer, cycle, model, option, i, optionSelected);
+        }
+
+        // A PropertySelection doesn't allow a body, so no need to worry about
+        // wrapped components.
+        renderer.endRender(this, writer, cycle);
+    }
+
+    public abstract IPropertySelectionModel getModel();
+
+    public abstract IMultiplePropertySelectionRenderer getRenderer();
+
+    public abstract void setRenderer(IMultiplePropertySelectionRenderer renderer);
+
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/form/MultiplePropertySelection.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/form/MultiplePropertySelection.jwc
new file mode 100644
index 0000000..ae91cb2
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/form/MultiplePropertySelection.jwc
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.contrib.form.MultiplePropertySelection" 
+	allow-body="no" allow-informal-parameters="no">
+	
+  <parameter name="model" 
+  	type="org.apache.tapestry.form.IPropertySelectionModel" required="yes" direction="in"/>
+  	
+  <parameter name="selectedList" type="java.util.List" required="yes"/>
+  
+  <parameter name="disabled" type="boolean" direction="in"/>
+  
+  <parameter name="renderer" 
+  	type="org.apache.tapestry.contrib.form.IMultiplePropertySelectionRenderer"
+  	direction="in"/>
+  	
+  <property-specification name="name" type="java.lang.String"/>
+  <property-specification name="form" type="org.apache.tapestry.IForm"/>
+  
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/informal/InheritInformalAny.java b/tapestry-contrib/src/org/apache/tapestry/contrib/informal/InheritInformalAny.java
new file mode 100644
index 0000000..f50d4b9
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/informal/InheritInformalAny.java
@@ -0,0 +1,117 @@
+//  Copyright 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.tapestry.contrib.informal;
+
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.IAsset;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.spec.IComponentSpecification;
+
+/**
+ * 
+ *  A version of the Any component that inherits the informal attributes of its parent.
+ *  This component has been deprecated in favour of the 'inherit-informal-parameters' 
+ *  tag that indicates that a particular component must inherit the informal parameters
+ *  of its parent. This tag is available in the page or component specification file.
+ * 
+ *  @deprecated
+ *  @version $Id$
+ *  @author mindbridge
+ *  @since 2.2
+ * 
+ **/
+
+public class InheritInformalAny extends AbstractComponent
+{
+    // Bindings
+    private IBinding m_objElementBinding;
+
+    public IBinding getElementBinding()
+    {
+        return m_objElementBinding;
+    }
+
+    public void setElementBinding(IBinding objElementBinding)
+    {
+        m_objElementBinding = objElementBinding;
+    }
+
+    protected void generateParentAttributes(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        String attribute;
+
+        IComponent objParent = getContainer();
+        if (objParent == null)
+            return;
+
+        IComponentSpecification specification = objParent.getSpecification();
+        Map bindings = objParent.getBindings();
+        if (bindings == null)
+            return;
+
+        Iterator i = bindings.entrySet().iterator();
+
+        while (i.hasNext())
+        {
+            Map.Entry entry = (Map.Entry) i.next();
+            String name = (String) entry.getKey();
+
+            // Skip over formal parameters stored in the bindings
+            // Map.  We're just interested in informal parameters.
+
+            if (specification.getParameter(name) != null)
+                continue;
+
+            IBinding binding = (IBinding) entry.getValue();
+
+            Object value = binding.getObject();
+            if (value == null)
+                continue;
+
+            if (value instanceof IAsset)
+            {
+                IAsset asset = (IAsset) value;
+
+                // Get the URL of the asset and insert that.
+                attribute = asset.buildURL(cycle);
+            }
+            else
+                attribute = value.toString();
+
+            writer.attribute(name, attribute);
+        }
+
+    }
+
+    public void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        String strElement = m_objElementBinding.getObject().toString();
+
+        writer.begin(strElement);
+        generateParentAttributes(writer, cycle);
+        renderInformalParameters(writer, cycle);
+
+        renderBody(writer, cycle);
+
+        writer.end();
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/informal/InheritInformalAny.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/informal/InheritInformalAny.jwc
new file mode 100644
index 0000000..e9d9eb1
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/informal/InheritInformalAny.jwc
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!--  $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.contrib.informal.InheritInformalAny" allow-body="yes" allow-informal-parameters="yes">
+    <parameter name="element" type="java.lang.String" required="yes" direction="custom"/>
+</component-specification>
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Engine_HRp4.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Engine_HRp4.gif
new file mode 100644
index 0000000..42ff5ec
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Engine_HRp4.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Engine_Hp3.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Engine_Hp3.gif
new file mode 100644
index 0000000..5f2860d
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Engine_Hp3.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Engine_NBanner.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Engine_NBanner.gif
new file mode 100644
index 0000000..0105a1b
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Engine_NBanner.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Engine_NRp2.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Engine_NRp2.gif
new file mode 100644
index 0000000..8590996
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Engine_NRp2.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Engine_Np1.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Engine_Np1.gif
new file mode 100644
index 0000000..78ee58c
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Engine_Np1.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Inspector.css b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Inspector.css
new file mode 100644
index 0000000..c8852b0
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Inspector.css
@@ -0,0 +1,201 @@
+H1  {

+	font-size: 12pt;

+	font-weight: bold;

+

+}

+

+H2  {}

+

+H3  {}

+

+A  {

+	color:#ffffff;

+}

+

+A:Visited  {}

+

+A:Active  {}

+

+A:Hover  {}

+

+SPAN.error

+{

+		color: Red;

+		font-weight: bold;

+	background-color : "#330066"		

+}

+

+BODY  {

+	font-family: "Trebuchet MS", sans-serif;

+	 background-color: #839cd1;

+}

+

+

+TABLE.inspector-data TR.odd TD  {

+	text-align : left;

+	color : Black;

+	background-color : Silver;

+}

+

+

+TABLE.inspector-data TR.even TH

+{

+	text-align : right;

+	font-weight: bold;

+}

+

+TABLE.inspector-data TR.odd TH

+{

+	text-align: right;

+	color : Black;

+	background-color : Silver;

+	font-weight: bold;

+}

+

+TABLE.inspector-data TR.even TD  {

+	text-align : left;

+}

+

+TABLE.inspector-data,

+TABLE.selector

+{

+	font-size: 9pt;

+}

+

+TABLE.selector TD.page-link

+{

+	font-style: italic;

+}

+

+TABLE.selector TD

+{

+	verticle-align: center;

+}

+

+TABLE.inspector-data TR.heading TH,

+TABLE.template TH

+{

+	text-align: center;

+	color : White;

+	background-color : "#330066";

+	font-weight: bold;

+}

+

+TABLE.template TD

+{

+	background-color: Silver;

+	font-size: small;

+}

+

+SPAN.message

+{

+	color : Silver

+	font-size: large;

+}

+

+SPAN.jwc-tag

+{

+	font-weight: bold;

+}

+

+SPAN.jwc-id, SPAN.localized-string

+{

+	font-style: italic;

+}

+

+

+

+TABLE.request-context-border  {

+	border-width : 1;

+	border-color : Black;

+	font-size: 9pt;

+}

+

+SPAN.request-context-object  {

+	font-weight : bold;

+	text-align : left;

+	font-size: 12pt;

+}

+

+TR.request-context-section TH  {

+	text-align : center;

+	color : White;

+	background-color : Blue;

+}

+

+TR.request-context-header TH  {

+	text-align : center;

+	color : White;

+	background-color : Blue;

+}

+

+TABLE.request-context-object TR.odd TD  {

+	text-align : left;

+	color : Black;

+	background-color : Silver;

+}

+

+TABLE.request-context-object TR.odd TH  {

+	color : Black;

+	background-color : Silver;

+	text-align : right;

+}

+

+TABLE.request-context-object TR.even TD  {

+	text-align : left;

+}

+

+TABLE.request-context-object TR.even TH  {

+	text-align : right;

+}

+

+TABLE.request-context-object  {

+	width : 100%;

+	font-size: 9pt;

+}

+

+TABLE.request-context-object TR  {

+	vertical-align : text-top;

+}

+

+TABLE.exception-display TR.even  {

+	top : auto;

+}

+

+TABLE.exception-displaY TD

+{

+	width: 100%;

+}

+

+TABLE.exception-display TR.even TH  {

+	text-align : right;

+	font-weight : bold;

+}

+

+TABLE.exception-display TR.odd TD  {

+	text-align : left;

+	background-color : Silver;

+}

+

+TABLE.exception-display TR.odd TH  {

+	text-align : right;

+	font-weight : bold;

+	background-color : Silver;	

+}

+

+TABLE.exception-display TR.even TD  {

+	text-align : left;

+}

+

+TABLE.exception-display TR.stack-trace  {

+	font-size : small;

+	font-family : sans-serif;

+	text-align : left;

+}

+

+UL 

+{

+	margin-top: 0px;

+	margin-bottom: 0px;

+	margin-left: 20px;

+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Inspector.html b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Inspector.html
new file mode 100644
index 0000000..42d16d6
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Inspector.html
@@ -0,0 +1,31 @@
+<!-- $Id$ -->
+<span jwcid="@Shell" title="ognl:inspectorTitle" stylesheet="ognl:assets.stylesheet">
+<body jwcid="@Body">
+
+<span jwcid="@inspector:Selector">
+
+<span jwcid="@inspector:ViewTabs">
+
+<span jwcid="@RenderBlock" block="ognl:blockForView"/>
+
+</span>
+</span>
+
+<span jwcid="specificationBlock@Block">
+<span jwcid="@inspector:ShowSpecification"/>
+</span>
+
+<span jwcid="templateBlock@Block">
+<span jwcid="@inspector:ShowTemplate"/>
+</span>
+
+<span jwcid="propertiesBlock@Block">
+<span jwcid="@inspector:ShowProperties"/>
+</span>
+
+<span jwcid="engineBlock@Block">
+<span jwcid="@inspector:ShowEngine"/>
+</span>
+
+</body>
+</span>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Inspector.java b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Inspector.java
new file mode 100644
index 0000000..631e893
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Inspector.java
@@ -0,0 +1,145 @@
+//  Copyright 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.tapestry.contrib.inspector;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IPage;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.components.Block;
+import org.apache.tapestry.html.BasePage;
+
+/**
+ *  The Tapestry Inspector page.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ **/
+
+public abstract class Inspector extends BasePage
+{
+    private Map _blocks = new HashMap();
+
+    protected void finishLoad()
+    {
+        _blocks.put(View.TEMPLATE, getComponent("templateBlock"));
+        _blocks.put(View.SPECIFICATION, getComponent("specificationBlock"));
+        _blocks.put(View.ENGINE, getComponent("engineBlock"));
+        _blocks.put(View.PROPERTIES, getComponent("propertiesBlock"));
+    }
+
+    public abstract View getView();
+
+    public abstract void setView(View value);
+
+    public abstract String getInspectedPageName();
+    
+    public abstract void setInspectedPageName(String value);
+
+    public abstract String getInspectedIdPath();
+
+    public abstract void setInspectedIdPath(String value);
+
+    /** 
+     *  Invoked to change the component being inspected within the current
+     *  page.
+     *
+     *  @since 1.0.6
+     **/
+
+    public void selectComponent(String idPath)
+    {
+        setInspectedIdPath(idPath);
+    }
+
+    /**
+     *  Method invoked by the {@link InspectorButton} component, 
+     *  to begin inspecting a page.
+     *
+     **/
+
+    public void inspect(String pageName, IRequestCycle cycle)
+    {
+        setInspectedPageName(pageName);
+        selectComponent((String) null);
+
+        cycle.activate(this);
+    }
+
+    /**
+     *  Listener for the component selection, which allows a particular component.  
+     *  
+     *  <p>The context is a single string,
+     *  the id path of the component to be selected (or null to inspect
+     *  the page itself).  This invokes
+     *  {@link #selectComponent(String)}.
+     *
+     **/
+
+    public void selectComponent(IRequestCycle cycle)
+    {
+        Object[] parameters = cycle.getServiceParameters();
+
+        String newIdPath;
+
+        // The up button may generate a null context.
+
+        if (parameters == null)
+            newIdPath = null;
+        else
+            newIdPath = (String) parameters[0];
+
+        selectComponent(newIdPath);
+    }
+
+    /**
+     *  Returns the {@link IPage} currently inspected by the Inspector, as determined
+     *  from the inspectedPageName property.
+     *
+     **/
+
+    public IPage getInspectedPage()
+    {
+        return getRequestCycle().getPage(getInspectedPageName());
+    }
+
+    /**
+     *  Returns the {@link IComponent} current inspected; this is determined
+     *  from the inspectedPageName and inspectedIdPath properties.
+     *
+     **/
+
+    public IComponent getInspectedComponent()
+    {
+        return getInspectedPage().getNestedComponent(getInspectedIdPath());
+    }
+
+    public String getInspectorTitle()
+    {
+        return "Tapestry Inspector: " + getEngine().getSpecification().getName();
+    }
+
+    /**
+     *  Returns the {@link Block} for the currently selected view.
+     *
+     **/
+
+    public Block getBlockForView()
+    {
+        return (Block) _blocks.get(getView());
+    }
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Inspector.library b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Inspector.library
new file mode 100644
index 0000000..17e172a
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Inspector.library
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Copyright 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.
+-->
+<!-- Inspector.library,v 1.1 2002/08/24 16:07:50 hship Exp -->
+<!DOCTYPE library-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<library-specification/>
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Inspector.page b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Inspector.page
new file mode 100644
index 0000000..03b88a4
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Inspector.page
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE page-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<page-specification class="org.apache.tapestry.contrib.inspector.Inspector">
+
+  <property-specification name="view" type="org.apache.tapestry.contrib.inspector.View"
+  		persistent="yes"
+  		initial-value="@org.apache.tapestry.contrib.inspector.View@SPECIFICATION"/>
+  <property-specification name="inspectedPageName" type="java.lang.String" persistent="yes"/>
+  <property-specification name="inspectedIdPath" type="java.lang.String" persistent="yes"/>
+  
+  <private-asset name="stylesheet" resource-path="Inspector.css"/>
+
+</page-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/InspectorButton.html b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/InspectorButton.html
new file mode 100644
index 0000000..aa38e55
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/InspectorButton.html
@@ -0,0 +1,10 @@
+<!-- $Id$ -->
+
+<span jwcid="$content$">
+
+<div id="tapestryInspector" style="position:absolute; border-color:black; border-width:2px; border-style:solid; padding:3px; background-color:#839cd1;">
+<a jwcid="link"><img jwcid="rollover"/></a>
+</div>
+
+</span>
+
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/InspectorButton.java b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/InspectorButton.java
new file mode 100644
index 0000000..9fd512d
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/InspectorButton.java
@@ -0,0 +1,133 @@
+//  Copyright 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.tapestry.contrib.inspector;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.IDirect;
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.IScript;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.engine.IEngineService;
+import org.apache.tapestry.engine.ILink;
+import org.apache.tapestry.engine.IScriptSource;
+import org.apache.tapestry.html.Body;
+
+/**
+ *  Component that can be placed into application pages that will launch
+ *  the inspector in a new window.
+ * 
+ *  [<a href="../../../../../../ComponentReference/InspectorButton.html">Component Reference</a>]
+ *
+ *  <p>Because the InspectorButton component is implemented using a {@link org.apache.tapestry.html.Rollover},
+ *  the containing page must use a {@link Body} component instead of
+ *  a &lt;body&gt; tag.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public class InspectorButton extends BaseComponent implements IDirect
+{
+    private boolean _disabled = false;
+
+    /**
+     *  Gets the listener for the link component.
+     *
+     *  @since 1.0.5
+     **/
+
+    public void trigger(IRequestCycle cycle)
+    {
+        String name = getNamespace().constructQualifiedName("Inspector");
+
+        Inspector inspector = (Inspector) cycle.getPage(name);
+
+        inspector.inspect(getPage().getPageName(), cycle);
+    }
+
+    /**
+     *  Renders the script, then invokes the normal implementation.
+     *
+     *  @since 1.0.5
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        if (_disabled || cycle.isRewinding())
+            return;
+
+        IEngine engine = getPage().getEngine();
+        IScriptSource source = engine.getScriptSource();
+
+        IResourceLocation scriptLocation =
+            getSpecification().getSpecificationLocation().getRelativeLocation(
+                "InspectorButton.script");
+
+        IScript script = source.getScript(scriptLocation);
+
+        Map symbols = new HashMap();
+
+        IEngineService service = engine.getService(Tapestry.DIRECT_SERVICE);
+        ILink link = service.getLink(cycle, this, null);
+
+        symbols.put("URL", link.getURL());
+
+        Body body = Body.get(cycle);
+
+        if (body == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("InspectorButton.must-be-contained-by-body"),
+                this,
+                null,
+                null);
+
+        script.execute(cycle, body, symbols);
+
+        // Now, go render the rest from the template.
+
+        super.renderComponent(writer, cycle);
+    }
+
+    public boolean isDisabled()
+    {
+        return _disabled;
+    }
+
+    public void setDisabled(boolean disabled)
+    {
+        _disabled = disabled;
+    }
+
+    /**
+     *  Always returns false.
+     * 
+     *  @since 2.3
+     * 
+     **/
+
+    public boolean isStateful()
+    {
+        return false;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/InspectorButton.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/InspectorButton.jwc
new file mode 100644
index 0000000..cccf00d
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/InspectorButton.jwc
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.contrib.inspector.InspectorButton" 
+	allow-body="no" 
+	allow-informal-parameters="no">
+
+    <description>
+<![CDATA[
+Includes the Inspector button on the page (which dynamically positions itself in the
+lower right corner).  Clicking the button raises the Tapestry Inspector in a pop-up
+window.
+]]>
+    </description>
+
+  <parameter name="disabled" type="boolean" direction="in"/>
+
+    <component id="link" type="GenericLink">
+        <static-binding name="href">javascript:ti_raiseInspector();</static-binding>
+    </component>
+
+    <component id="rollover" type="Rollover">
+    	<binding name="image" expression="assets.logo"/>
+    	<binding name="focus" expression="assets.inspector"/>
+    </component>
+
+    <private-asset name="logo" resource-path="tapestry-logo.gif"/>
+    <private-asset name="inspector" resource-path="inspector-rollover.gif"/>
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/InspectorButton.script b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/InspectorButton.script
new file mode 100644
index 0000000..a0e1f31
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/InspectorButton.script
@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- $Id$ -->
+<!DOCTYPE script PUBLIC 
+	"-//Howard Lewis Ship//Tapestry Script 1.2//EN"
+	"http://tapestry.sf.net/dtd/Script_1_2.dtd">
+<!--
+
+Adds scripting support for the ShowInspector component.
+
+Prefixes all variables and functions with "ti_" (for Tapestry Inspector).
+
+Expects that the Inspector is inside a <div> named "tapestryInspector".
+
+Input symbols:
+ URL - The complete URL needed for to raise the Inspector
+ 
+-->
+<script>
+
+<include-script resource-path="/org/apache/tapestry/html/PracticalBrowserSniffer.js"/>
+
+<input-symbol key="URL" class="java.lang.String" required="yes"/>
+
+<body>
+var ti = new Object();
+
+ti.oldX = 0;
+ti.oldY = 0;
+
+function ti_positionInspector()
+{
+  var object;
+  var width;
+  var height;
+
+  if (navigator.family == "nn4")
+  {
+    object = document.tapestryInspector;
+    width = innerWidth + pageXOffset;  <!-- Doesn't properly account for scrollbars! -->
+    height = innerHeight + pageYOffset;
+  }
+  else
+  {
+    object = document.getElementById("tapestryInspector");
+    
+	if (navigator.OS == "mac")
+	{
+	  width = document.body.offsetWidth;
+	  height = document.body.offsetWidth;
+	}
+	else if (navigator.family == "gecko")
+	{
+      width = innerWidth + pageXOffset; 
+      height = innerHeight + pageYOffset;
+	}
+	else
+	{
+	  // IE 5, 6? on PC
+	  width = document.body.clientWidth  + document.body.scrollLeft;
+	  height = document.body.clientHeight + document.body.scrollTop;
+	}
+  }
+   	
+  // The width/height of the animation, plus
+  // a couple of pixels of border.
+  
+  var indent = 65;
+  
+  var x = width - indent;
+  var y = height - indent;
+
+  if (navigator.family == "nn4")
+  {
+    if (x != ti.oldX || y != ti.oldY)
+    {
+      object.moveTo(x, y);
+      object.visibility = "visible";
+    }
+  }
+  else
+  {
+    if (x != ti.oldX)
+    {
+       object.style.left = x + "px";
+       ti.oldX = x;
+    }
+    if (y != ti.oldY)
+    {
+    	object.style.top = y + "px";
+    	ti.oldY = y;
+    }
+
+ 	object.style.visibility = "visible";  
+  }
+
+
+    
+  // Reposition it every quarter second.
+  
+  window.setTimeout("ti_positionInspector()", 250);
+}
+
+function ti_raiseInspector()
+{
+  var newWindow = window.open(
+  	"${URL}",
+  	"TapestryInspector",
+  	"titlebar,resizable,scrollbars,width=700,height=600");
+  	
+  newWindow.focus();
+}
+</body>
+
+<initialization>
+ti_positionInspector();
+</initialization>
+
+</script>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Properties_HRp4.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Properties_HRp4.gif
new file mode 100644
index 0000000..e2fe821
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Properties_HRp4.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Properties_Hp3.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Properties_Hp3.gif
new file mode 100644
index 0000000..8aa25cb
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Properties_Hp3.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Properties_NBanner.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Properties_NBanner.gif
new file mode 100644
index 0000000..5fa7f5b
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Properties_NBanner.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Properties_NRp2.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Properties_NRp2.gif
new file mode 100644
index 0000000..12b13a1
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Properties_NRp2.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Properties_Np1.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Properties_Np1.gif
new file mode 100644
index 0000000..917436a
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Properties_Np1.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Reset_NRp2.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Reset_NRp2.gif
new file mode 100644
index 0000000..371cba0
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Reset_NRp2.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Reset_Np1.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Reset_Np1.gif
new file mode 100644
index 0000000..eb87bf4
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Reset_Np1.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Reset_Np1_disabled.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Reset_Np1_disabled.gif
new file mode 100644
index 0000000..0cfdd51
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Reset_Np1_disabled.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Restart_NRp2.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Restart_NRp2.gif
new file mode 100644
index 0000000..8fed715
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Restart_NRp2.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Restart_Np1.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Restart_Np1.gif
new file mode 100644
index 0000000..dda92b0
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Restart_Np1.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Selector.html b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Selector.html
new file mode 100644
index 0000000..15ecbf7
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Selector.html
@@ -0,0 +1,23 @@
+<!-- $Id$ -->
+
+<table class="selector">
+	<tr valign=center>
+		<td>
+			<form jwcid="form">
+				<select jwcid="selectPage"/>
+			</form>
+		</td>
+		<td class="page-link">
+			<a jwcid="page">page</a>
+		</td>
+<span jwcid="e">
+		<td>&gt;&gt;</td>
+		<td>
+		<a jwcid="component"><span jwcid="insertId"/></a>
+		</td>
+</span>
+	</tr>
+</table>
+
+<span jwcid="renderBody"/>
+
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Selector.java b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Selector.java
new file mode 100644
index 0000000..158025d
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Selector.java
@@ -0,0 +1,150 @@
+//  Copyright 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.tapestry.contrib.inspector;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.INamespace;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.engine.ISpecificationSource;
+import org.apache.tapestry.form.IPropertySelectionModel;
+import org.apache.tapestry.form.StringPropertySelectionModel;
+
+/**
+ *  Component of the {@link Inspector} page used to select the page and "crumb trail"
+ *  of the inspected component.
+ *
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public class Selector extends BaseComponent
+{
+    /**
+     *  When the form is submitted,
+     *  the inspectedPageName of the {@link Inspector} page will be updated,
+     *  but we need to reset the inspectedIdPath as well.
+     *
+     **/
+
+    public void formSubmit(IRequestCycle cycle)
+    {
+        Inspector inspector = (Inspector) getPage();
+
+        inspector.selectComponent((String) null);
+    }
+
+    /**
+     *  Returns an {IPropertySelectionModel} used to select the name of the page
+     *  to inspect.  The page names are sorted.
+     *
+     **/
+
+    public IPropertySelectionModel getPageModel()
+    {
+        return new StringPropertySelectionModel(getPageNames());
+    }
+
+    /**
+     *  The crumb trail is all the components from the inspected component up to
+     *  (but not including) the page.
+     *
+     **/
+
+    public List getCrumbTrail()
+    {
+        List result = null;
+
+        Inspector inspector = (Inspector) getPage();
+        IComponent component = inspector.getInspectedComponent();
+        IComponent container = null;
+
+        while (true)
+        {
+            container = component.getContainer();
+            if (container == null)
+                break;
+
+            if (result == null)
+                result = new ArrayList();
+
+            result.add(component);
+
+            component = container;
+        }
+
+        if (result == null)
+            return null;
+
+        // Reverse the list, such that the inspected component is last, and the
+        // top-most container is first.
+
+        Collections.reverse(result);
+
+        return result;
+    }
+
+    private String[] getPageNames()
+    {
+        Set names = new HashSet();
+
+        ISpecificationSource source = getPage().getEngine().getSpecificationSource();
+
+        addPageNames(names, source.getFrameworkNamespace());
+        addPageNames(names, source.getApplicationNamespace());
+
+        List l = new ArrayList(names);
+        Collections.sort(l);
+
+        return (String[]) l.toArray(new String[l.size()]);
+    }
+
+    private void addPageNames(Set names, INamespace namespace)
+    {
+        String idPrefix = namespace.getExtendedId();
+
+        List pageNames = namespace.getPageNames();
+        int count = pageNames.size();
+
+        for (int i = 0; i < count; i++)
+        {
+            String name = (String) pageNames.get(i);
+
+            if (idPrefix == null)
+                names.add(name);
+            else
+                names.add(idPrefix + ":" + name);
+        }
+
+        List ids = namespace.getChildIds();
+        count = ids.size();
+
+        for (int i = 0; i < count; i++)
+        {
+            String id = (String) ids.get(i);
+
+            addPageNames(names, namespace.getChildNamespace(id));
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Selector.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Selector.jwc
new file mode 100644
index 0000000..dee4b40
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Selector.jwc
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.contrib.inspector.Selector">
+
+  <component id="form" type="Form">
+    <binding name="listener" expression="listeners.formSubmit"/>
+  </component>
+
+  <component id="selectPage" type="PropertySelection">
+    <binding name="value" expression="page.inspectedPageName"/>
+    <binding name="model" expression="pageModel"/>
+    <binding name="submitOnChange" expression="true"/>
+  </component>
+
+  <component id="page" type="DirectLink">
+    <binding name="listener" expression="page.listeners.selectComponent"/>
+  </component>
+
+  <component id="e" type="Foreach">
+    <binding name="source" expression="crumbTrail"/>
+  </component>
+
+  <component id="component" type="DirectLink">
+    <binding name="parameters" expression="components.e.value.idPath"/>
+    <binding name="listener" expression="page.listeners.selectComponent"/>
+  </component>
+
+  <component id="insertId" type="Insert">
+    <binding name="value" expression="components.e.value.id"/>
+  </component>
+
+  <component id="renderBody" type="RenderBody"/>
+
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowDescription.html b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowDescription.html
new file mode 100644
index 0000000..917e59c
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowDescription.html
@@ -0,0 +1,5 @@
+<!-- $Id$ -->
+
+<span jwcid="$content$">
+<span jwcid="ifDescription"><img jwcid="descriptionImage"/></span>
+</span>
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowDescription.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowDescription.jwc
new file mode 100644
index 0000000..28466ca
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowDescription.jwc
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.BaseComponent" 
+	allow-body="no" 
+	allow-informal-parameters="no">
+	
+	<parameter name="description" required="yes"/>
+	
+	<component id="ifDescription" type="Conditional">
+	  <inherited-binding name="condition" parameter-name="description"/>
+	</component>
+	
+	<component id="descriptionImage" type="Image">
+	  <binding name="image" expression="assets.info"/>
+	  <inherited-binding name="alt" parameter-name="description"/>
+	</component>
+	
+	<private-asset name="info" resource-path="info.gif"/>
+	
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowEngine.html b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowEngine.html
new file mode 100644
index 0000000..797c8ff
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowEngine.html
@@ -0,0 +1,90 @@
+<!-- $Id$ -->
+
+<table class="inspector-data">
+
+	<tr class="heading">
+		<th colspan=2>Engine/Application Properties</th>
+	</tr>
+	
+	<tr class="heading">
+		<th>Name</th> <th>Property</th>
+	</tr>
+	
+	<tr class="even">
+		<th>Tapestry Framework Version</th>
+		<td><span jwcid="insertFrameworkVersion"/></td>
+	</tr>
+	
+	<tr class="odd">
+		<th>Application Name</th>
+		<td><span jwcid="insertApplicationName"/></td>
+	</tr>
+
+	<tr class="even">
+		<th>Context Path</th>
+		<td><span jwcid="insertContextPath"/></td>
+	</tr>
+
+	<tr class="odd">
+		<th>Servlet Path</th>
+		<td><span jwcid="insertServletPath"/></td>
+	</tr>
+
+	<tr class="even">
+		<th>Engine Class</th>
+		<td><span jwcid="insertEngineClass"/></td>
+	</tr>
+	
+	<tr class="odd">
+		<th>Locale</th>
+		<td><span jwcid="insertLocale"/></td>
+	</tr>
+
+	<tr class="even">
+		<th>Visit</th>
+		<td>
+<span jwcid="ifNoVisit">
+<em>none</em>
+</span>
+
+<span jwcid="ifVisit">
+<span jwcid="insertVisit"/>
+</span>
+		</td>
+	</tr>
+
+</table>
+
+<h1>Operations</h1>
+
+<table class="inspector-data">
+
+	<tr class="even">
+		<td><a jwcid="restart" target="_new"><img jwcid="restartButton"/></a>
+		</td>
+		<td>Restart the application (in a new window).
+		</td>
+	</tr>
+
+	<tr class="even">
+		<td><a jwcid="reset"><img jwcid="resetButton"/></a>
+		</td>
+		<td>
+		Reset the application, discarding all cached specifications, assets
+		and templates.
+		</td>
+	</tr>
+</table>
+
+<h1>Serialized Engine</h1>
+
+<p>The serialized state of the application engine (the size of this is relevant
+for application servers which support clustering).
+
+<p><span jwcid="insertByteCount"/> bytes:
+<pre><span jwcid="insertSerializedEngine"/></pre>
+
+<h1>Request Context</h1>
+
+<span jwcid="insertRequest"/>
+
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowEngine.java b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowEngine.java
new file mode 100644
index 0000000..0b9dc71
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowEngine.java
@@ -0,0 +1,186 @@
+//  Copyright 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.tapestry.contrib.inspector;
+
+import java.io.ByteArrayOutputStream;
+import java.io.CharArrayWriter;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.event.PageDetachListener;
+import org.apache.tapestry.event.PageEvent;
+import org.apache.tapestry.util.io.BinaryDumpOutputStream;
+
+/**
+ *  Component of the {@link Inspector} page used to display
+ *  the properties of the {@link org.apache.tapestry.IEngine} as well as a serialized view of it.
+ *  Also, the {@link org.apache.tapestry.request.RequestContext} is dumped out.
+ *
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public class ShowEngine extends BaseComponent implements PageDetachListener
+{
+    private byte[] serializedEngine;
+
+    public void pageDetached(PageEvent event)
+    {
+        serializedEngine = null;
+    }
+
+    /**
+     *  Workaround for OGNL limitation --- OGNL can't dereference
+     *  past class instances.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public String getEngineClassName()
+    {
+        return getPage().getEngine().getClass().getName();
+    }
+
+    private byte[] getSerializedEngine()
+    {
+        if (serializedEngine == null)
+            buildSerializedEngine();
+
+        return serializedEngine;
+    }
+
+    private void buildSerializedEngine()
+    {
+        ByteArrayOutputStream bos = null;
+        ObjectOutputStream oos = null;
+
+        try
+        {
+            bos = new ByteArrayOutputStream();
+            oos = new ObjectOutputStream(bos);
+
+            // Write the application object to the stream.
+
+            oos.writeObject(getPage().getEngine());
+
+            // Extract the application as an array of bytes.
+
+            serializedEngine = bos.toByteArray();
+        }
+        catch (IOException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("ShowEngine.could-not-serialize"),
+                ex);
+        }
+        finally
+        {
+            close(oos);
+            close(bos);
+        }
+
+        // It would be nice to deserialize the application object now, but in
+        // practice, that fails due to class loader problems.
+    }
+
+    private void close(OutputStream stream)
+    {
+        if (stream == null)
+            return;
+
+        try
+        {
+            stream.close();
+        }
+        catch (IOException ex)
+        {
+            // Ignore.
+        }
+    }
+
+    public int getEngineByteCount()
+    {
+        return getSerializedEngine().length;
+    }
+
+    public IRender getEngineDumpDelegate()
+    {
+        return new IRender()
+        {
+            public void render(IMarkupWriter writer, IRequestCycle cycle)
+            {
+                dumpSerializedEngine(writer);
+            }
+        };
+    }
+
+    private void dumpSerializedEngine(IMarkupWriter responseWriter)
+    {
+        CharArrayWriter writer = null;
+        BinaryDumpOutputStream bos = null;
+
+        try
+        {
+            // Because IReponseWriter doesn't implement the
+            // java.io.Writer interface, we have to buffer this
+            // stuff then pack it in all at once.  Kind of a waste!
+
+            writer = new CharArrayWriter();
+
+            bos = new BinaryDumpOutputStream(writer);
+            bos.setBytesPerLine(32);
+
+            bos.write(getSerializedEngine());
+            bos.close();
+
+            responseWriter.print(writer.toString());
+        }
+        catch (IOException ex)
+        {
+            // Ignore.
+        }
+        finally
+        {
+            if (bos != null)
+            {
+                try
+                {
+                    bos.close();
+                }
+                catch (IOException ex)
+                {
+                    // Ignore.
+                }
+            }
+
+            if (writer != null)
+            {
+                writer.reset();
+                writer.close();
+            }
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowEngine.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowEngine.jwc
new file mode 100644
index 0000000..b181816
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowEngine.jwc
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.contrib.inspector.ShowEngine" allow-body="no" allow-informal-parameters="no">
+
+  <component id="insertFrameworkVersion" type="Insert">
+  	<binding name="value" expression="@org.apache.tapestry.Tapestry@VERSION"/>
+  </component>
+  
+  <component id="insertApplicationName" type="Insert">
+    <binding name="value" expression="page.engine.specification.name"/>
+  </component>
+  
+  <component id="insertContextPath" type="Insert">
+    <binding name="value" expression="page.engine.contextPath"/>
+  </component>
+  
+  <component id="insertServletPath" type="Insert">
+    <binding name="value" expression="page.engine.servletPath"/>
+  </component>
+  
+  <component id="insertEngineClass" type="Insert">
+    <binding name="value" expression="engineClassName"/>
+  </component>
+  
+  <component id="insertLocale" type="Insert">
+    <binding name="value" expression="page.engine.locale.displayName"/>
+  </component>
+  
+  <component id="ifNoVisit" type="Conditional">
+    <binding name="condition" expression="! page.engine.hasVisit"/>
+  </component>
+  
+  <component id="insertVisit" type="Insert">
+    <binding name="value" expression="page.engine.visit"/>
+  </component>
+  
+  <component id="ifVisit" type="Conditional">
+    <binding name="condition" expression="page.engine.hasVisit"/>
+  </component>
+  
+  <component id="restart" type="ServiceLink">
+    <binding name="service" expression="@org.apache.tapestry.Tapestry@RESTART_SERVICE"/>
+  </component>
+  
+  <component id="restartButton" type="Rollover">
+    <binding name="image" expression="assets.restart"/>
+    <binding name="focus" expression="assets.restartFocus"/>
+  </component>
+  
+  <component id="reset" type="ServiceLink">
+    <binding name="service" expression="@org.apache.tapestry.Tapestry@RESET_SERVICE"/>
+    <binding name="disabled" expression="! page.engine.resetServiceEnabled"/>
+  </component>
+  
+  <component id="resetButton" type="Rollover">
+    <binding name="image" expression="assets.reset"/>
+    <binding name="focus" expression="assets.resetFocus"/>
+    <binding name="disabled" expression="assets.resetDisabled"/>
+  </component>
+  
+  <component id="insertByteCount" type="Insert">
+    <binding name="value" expression="engineByteCount"/>
+  </component>
+  
+  <component id="insertSerializedEngine" type="Delegator">
+    <binding name="delegate" expression="engineDumpDelegate"/>
+  </component>
+  
+  <component id="insertRequest" type="Delegator">
+    <binding name="delegate" expression="page.requestCycle.requestContext"/>
+  </component>
+  
+  <private-asset name="reset" resource-path="Reset_Np1.gif"/>
+  <private-asset name="resetFocus" resource-path="Reset_NRp2.gif"/>
+  <private-asset name="resetDisabled" resource-path="Reset_Np1_disabled.gif"/>
+  <private-asset name="restart" resource-path="Restart_Np1.gif"/>
+  <private-asset name="restartFocus" resource-path="Restart_NRp2.gif"/>
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowProperties.html b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowProperties.html
new file mode 100644
index 0000000..1aa67d8
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowProperties.html
@@ -0,0 +1,35 @@
+<!-- $Id$ -->
+
+<span jwcid="ifNoProperties">
+<span class="message">Page contains no persistent properties.</span>
+</span>
+
+<span jwcid="ifHasProperties">
+<table class="inspector-data">
+	<tr class="heading">
+		<th>Component</th> <th>Property Name</th> <th>Value Class</th> <th>Value</th>
+	</tr>
+
+	<tr jwcid="e">
+		<td>
+			<a jwcid="selectComponent"><span jwcid="insertPath"/></a>
+		</td>
+		<td>
+			<span jwcid="insertPersistPropertyName"/>
+		</td>
+		<td>
+			<span jwcid="insertPersistValueClass"/>
+		</td>
+		<td>
+			<span jwcid="insertPersistValue"/>
+		</td>
+	</tr>
+
+</table>
+
+
+</span>
+
+
+
+	
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowProperties.java b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowProperties.java
new file mode 100644
index 0000000..6b96c2c
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowProperties.java
@@ -0,0 +1,142 @@
+//  Copyright 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.tapestry.contrib.inspector;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.IPage;
+import org.apache.tapestry.engine.IPageRecorder;
+import org.apache.tapestry.event.PageEvent;
+import org.apache.tapestry.event.PageRenderListener;
+import org.apache.tapestry.record.IPageChange;
+
+/**
+ *  Component of the {@link Inspector} page used to display
+ *  the persisent properties of the page.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public class ShowProperties extends BaseComponent implements PageRenderListener
+{
+    private List _properties;
+    private IPageChange _change;
+    private IPage _inspectedPage;
+
+    /**
+     *  Does nothing.
+     *
+     *  @since 1.0.5
+     *
+     **/
+
+    public void pageBeginRender(PageEvent event)
+    {
+    }
+
+    /**
+     *  @since 1.0.5
+     *
+     **/
+
+    public void pageEndRender(PageEvent event)
+    {
+        _properties = null;
+        _change = null;
+        _inspectedPage = null;
+    }
+
+    private void buildProperties()
+    {
+        Inspector inspector = (Inspector) getPage();
+
+        _inspectedPage = inspector.getInspectedPage();
+
+        IEngine engine = getPage().getEngine();
+        IPageRecorder recorder =
+            engine.getPageRecorder(_inspectedPage.getPageName(), inspector.getRequestCycle());
+
+        // No page recorder?  No properties.
+
+        if (recorder == null)
+        {
+            _properties = Collections.EMPTY_LIST;
+            return;
+        }
+
+        if (recorder.getHasChanges())
+            _properties = new ArrayList(recorder.getChanges());
+    }
+
+    /**
+     *  Returns a {@link List} of {@link IPageChange} objects.
+     *
+     *  <p>Sort order is not defined.
+     *
+     **/
+
+    public List getProperties()
+    {
+        if (_properties == null)
+            buildProperties();
+
+        return _properties;
+    }
+
+    public void setChange(IPageChange value)
+    {
+        _change = value;
+    }
+
+    public IPageChange getChange()
+    {
+        return _change;
+    }
+
+    /**
+     *  Returns the name of the value's class, if the value is non-null.
+     *
+     **/
+
+    public String getValueClassName()
+    {
+        Object value;
+
+        value = _change.getNewValue();
+
+        if (value == null)
+            return "<null>";
+
+        return convertClassToName(value.getClass());
+    }
+
+    private String convertClassToName(Class cl)
+    {
+        // TODO: This only handles one-dimensional arrays
+        // property.
+
+        if (cl.isArray())
+            return "array of " + cl.getComponentType().getName();
+
+        return cl.getName();
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowProperties.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowProperties.jwc
new file mode 100644
index 0000000..4bde6af
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowProperties.jwc
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.contrib.inspector.ShowProperties">
+
+  <bean name="persistPropertyClass" class="org.apache.tapestry.bean.EvenOdd"/>
+  <bean name="propertyClass" class="org.apache.tapestry.bean.EvenOdd"/>
+  
+  <component id="ifNoProperties" type="Conditional">
+    <binding name="condition" expression="!properties"/>
+  </component>
+  
+  <component id="ifHasProperties" type="Conditional">
+    <binding name="condition" expression="properties"/>
+  </component>
+  
+  <component id="e" type="Foreach">
+    <binding name="source" expression="properties"/>
+    <binding name="value" expression="change"/>
+    <static-binding name="element">tr</static-binding>
+    <binding name="class" expression="beans.persistPropertyClass.next"/>
+  </component>
+  
+  <component id="selectComponent" type="DirectLink">
+    <binding name="listener" expression="page.listeners.selectComponent"/>
+    <binding name="parameters" expression="change.componentPath"/>
+    <binding name="disabled" expression="change.componentPath == null"/>
+  </component>
+  
+  <component id="insertPath" type="Insert">
+    <binding name="value" expression="change.componentPath"/>
+  </component>
+  
+  <component id="insertPersistPropertyName" type="Insert">
+    <binding name="value" expression="change.propertyName"/>
+  </component>
+  
+  <component id="insertPersistValueClass" type="Insert">
+    <binding name="value" expression="valueClassName"/>
+  </component>
+  
+  <component id="insertPersistValue" type="Insert">
+    <binding name="value" expression="change.newValue"/>
+  </component>
+  
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowSpecification.html b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowSpecification.html
new file mode 100644
index 0000000..74bdccf
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowSpecification.html
@@ -0,0 +1,161 @@
+<!-- $Id$ -->
+
+<table>
+<tr valign=top>
+<td>
+
+<table class="inspector-data" width="100%">
+	<tr class="even">
+		<th>Specification Resource Location</th>
+		<td><span jwcid="@Insert" value="ognl:inspectedSpecification.specificationLocation"/>
+		<span jwcid="@ShowDescription" description="ognl:inspectedSpecification.description"/>
+		</td>
+	</tr>
+	
+	<tr class="odd">
+		<th>Java class</th>
+		<td><span jwcid="@Insert" value="ognl:inspectedComponent.getClass().getName()"/></td>
+	</tr>
+
+<span jwcid="@Conditional" condition="ognl:! inspectedSpecification.pageSpecification">
+
+	<tr class="even">
+		<th>Allow informal parameters</th>
+		<td><span jwcid="@Insert" value="ognl:inspectedSpecification.allowInformalParameters"/></td>
+	</tr>
+
+	<tr class="odd">
+		<th>Allow body</th>
+		<td><span jwcid="@Insert" value="ognl:inspectedSpecification.allowBody"/></td>
+	</tr>
+	
+</span>
+	
+</table>
+
+<span jwcid="@Conditional" condition="ognl:formalParameterNames">
+
+<table class="inspector-data" width="100%">
+	<tr class="heading">
+		<th colspan=4>Formal Parameters</th>
+	</tr>
+	<tr class="heading">
+		<th>Name</th> <th>Required</th> <th>Java type</th> <th>Binding</th>
+	</tr>
+
+	<tr jwcid="e_formal">
+		<td><span jwcid="@Insert" value="ognl:parameterName"/>
+		<span jwcid="@ShowDescription" description="ognl:parameterSpecification.description"/>
+		</td>
+		<td><span jwcid="@Insert" value="ognl:parameterSpecification.required"/></td>
+		<td><span jwcid="@Insert" value="ognl:parameterSpecification.type"/></td>
+		<td><span jwcid="@Insert" value="ognl:binding"/></td>
+	</tr>
+
+</table>
+</span>
+
+<span jwcid="@Conditional" condition="ognl:informalParameterNames">
+
+<table class="inspector-data" width="100%">
+	<tr class="heading">
+		<th colspan=2>Informal Parameters</th>
+	</tr>
+	<tr class="heading">
+		<th>Name</th> <th>Binding</th>
+	</tr>
+
+	<tr jwcid="e_informal">
+		<td><span jwcid="@Insert" value="ognl:parameterName"/></td>
+		<td><span jwcid="@Insert" value="ognl:binding"/></td>
+	</tr>
+
+</table>
+</span>
+
+<span jwcid="@Conditional" condition="ognl:assetNames">
+
+<table class="inspector-data"  width="100%">
+	<tr class="heading">
+		<th colspan=2>Assets</th>
+	</tr>
+	<tr class="heading">
+		<th>Name</th> <th>Asset</th>
+	</tr>
+
+	<tr jwcid="e_asset">
+		<td><span jwcid="@Insert" value="ognl:assetName"/></td>
+		<td><span jwcid="@Insert" value="ognl:asset"/></td>
+	</tr>
+
+
+</table>
+</span>
+
+<span jwcid="@Conditional" condition="ognl:sortedPropertyNames">
+
+<table class="inspector-data" width="100%">
+	<tr class="heading">
+		<th colspan=2>Properties</th>
+	</tr>
+	<tr class="heading">
+		<th>Name</th> <th>Property</th>
+	</tr>
+
+	<tr jwcid="e_property">
+		<th><span jwcid="@Insert" value="ognl:propertyName"/></th>
+		<td><span jwcid="@Insert" value="ognl:propertyValue"/></td>
+	</tr>
+
+</table>
+</span>
+
+<span jwcid="@Conditional" condition="ognl:beanNames">
+
+<table class="inspector-data" width="100%">
+	<tr class="heading">
+		<th colspan=3>Helper Beans</th>
+	</tr>
+	
+	<tr class="heading">
+		<th>Name</th> <th>Class</th> <th>Lifecycle</th>
+	</tr>
+	
+	<tr jwcid="e_bean">
+		<td><span jwcid="@Insert" value="ognl:beanName"/></td>
+		<td><span jwcid="@Insert" value="ognl:beanSpecification.className"/></td>
+		<td><span jwcid="@Insert" value="ognl:beanSpecification.lifecycle.name"/></td>
+	</tr>
+	
+</table>
+</span>
+
+</td>
+
+<td>
+<span jwcid="@Conditional" condition="ognl:sortedComponents">
+
+<table border="0" class="inspector-data">
+
+	<tr class="heading">
+		<th colspan=2>Embedded Components</th>
+	</tr>
+	<tr class="heading">
+		<th>Id</th> <th>Type</th>
+	</tr>
+
+	<tr jwcid="e_components">
+		<td>
+			<a jwcid="selectComponent"><span jwcid="@Insert" value="ognl:component.id"/></a>
+		</td>
+		<td>
+			<span jwcid="@Insert" value="ognl:componentType"/>
+		</td>
+	</tr>
+
+</table>
+</span>
+
+</td>
+</tr>
+</table>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowSpecification.java b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowSpecification.java
new file mode 100644
index 0000000..098a6fa
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowSpecification.java
@@ -0,0 +1,366 @@
+//  Copyright 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.tapestry.contrib.inspector;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.IAsset;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.event.PageEvent;
+import org.apache.tapestry.event.PageRenderListener;
+import org.apache.tapestry.spec.IBeanSpecification;
+import org.apache.tapestry.spec.IComponentSpecification;
+import org.apache.tapestry.spec.IContainedComponent;
+import org.apache.tapestry.spec.IParameterSpecification;
+
+/**
+ *  Component of the {@link Inspector} page used to display
+ *  the specification, parameters and bindings and assets of the inspected component.
+ *
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public class ShowSpecification extends BaseComponent implements PageRenderListener
+{
+    private IComponent _inspectedComponent;
+    private IComponentSpecification _inspectedSpecification;
+    private String _parameterName;
+    private String _assetName;
+    private List _sortedComponents;
+    private IComponent _component;
+    private List _assetNames;
+    private List _formalParameterNames;
+    private List _informalParameterNames;
+    private List _sortedPropertyNames;
+    private String _propertyName;
+    private List _beanNames;
+    private String _beanName;
+    private IBeanSpecification _beanSpecification;
+
+    private static class ComponentComparitor implements Comparator
+    {
+        public int compare(Object left, Object right)
+        {
+            IComponent leftComponent;
+            String leftId;
+            IComponent rightComponent;
+            String rightId;
+
+            if (left == right)
+                return 0;
+
+            leftComponent = (IComponent) left;
+            rightComponent = (IComponent) right;
+
+            leftId = leftComponent.getId();
+            rightId = rightComponent.getId();
+
+            return leftId.compareTo(rightId);
+        }
+    }
+
+    /**
+     *  Clears all cached information about the component and such after
+     *  each render (including the rewind phase render used to process
+     *  the tab view).
+     *
+     *  @since 1.0.5
+     *
+     **/
+
+    public void pageEndRender(PageEvent event)
+    {
+        _inspectedComponent = null;
+        _inspectedSpecification = null;
+        _parameterName = null;
+        _assetName = null;
+        _sortedComponents = null;
+        _component = null;
+        _assetNames = null;
+        _formalParameterNames = null;
+        _informalParameterNames = null;
+        _sortedPropertyNames = null;
+        _propertyName = null;
+        _beanNames = null;
+        _beanName = null;
+        _beanSpecification = null;
+    }
+
+    /**
+     *  Gets the inspected component and specification from the {@link Inspector} page.
+     *
+     *  @since 1.0.5
+     **/
+
+    public void pageBeginRender(PageEvent event)
+    {
+        Inspector inspector = (Inspector) getPage();
+
+        _inspectedComponent = inspector.getInspectedComponent();
+        _inspectedSpecification = _inspectedComponent.getSpecification();
+    }
+
+    public IComponent getInspectedComponent()
+    {
+        return _inspectedComponent;
+    }
+
+    public IComponentSpecification getInspectedSpecification()
+    {
+        return _inspectedSpecification;
+    }
+
+    /**
+     *  Returns a sorted list of formal parameter names.
+     *
+     **/
+
+    public List getFormalParameterNames()
+    {
+        if (_formalParameterNames == null)
+            _formalParameterNames = sort(_inspectedSpecification.getParameterNames());
+
+        return _formalParameterNames;
+    }
+
+    /**
+     *  Returns a sorted list of informal parameter names.  This is
+     *  the list of all bindings, with the list of parameter names removed,
+     *  sorted.
+     *
+     **/
+
+    public List getInformalParameterNames()
+    {
+        if (_informalParameterNames != null)
+            return _informalParameterNames;
+
+        Collection names = _inspectedComponent.getBindingNames();
+        if (names != null && names.size() > 0)
+        {
+            _informalParameterNames = new ArrayList(names);
+
+            // Remove the names of any formal parameters.  This leaves
+            // just the names of informal parameters (informal parameters
+            // are any parameters/bindings that don't match a formal parameter
+            // name).
+
+            names = _inspectedSpecification.getParameterNames();
+            if (names != null)
+                _informalParameterNames.removeAll(names);
+
+            Collections.sort(_informalParameterNames);
+        }
+
+        return _informalParameterNames;
+    }
+
+    public String getParameterName()
+    {
+        return _parameterName;
+    }
+
+    public void setParameterName(String value)
+    {
+        _parameterName = value;
+    }
+
+    /**
+     *  Returns the {@link org.apache.tapestry.spec.ParameterSpecification} corresponding to
+     *  the value of the parameterName property.
+     *
+     **/
+
+    public IParameterSpecification getParameterSpecification()
+    {
+        return _inspectedSpecification.getParameter(_parameterName);
+    }
+
+    /**
+     *  Returns the {@link IBinding} corresponding to the value of
+     *  the parameterName property.
+     *
+     **/
+
+    public IBinding getBinding()
+    {
+        return _inspectedComponent.getBinding(_parameterName);
+    }
+
+    public void setAssetName(String value)
+    {
+        _assetName = value;
+    }
+
+    public String getAssetName()
+    {
+        return _assetName;
+    }
+
+    /**
+     *  Returns the {@link IAsset} corresponding to the value
+     *  of the assetName property.
+     *
+     **/
+
+    public IAsset getAsset()
+    {
+        return (IAsset) _inspectedComponent.getAssets().get(_assetName);
+    }
+
+    /**
+     *  Returns a sorted list of asset names, or null if the
+     *  component contains no assets.
+     *
+     **/
+
+    public List getAssetNames()
+    {
+        if (_assetNames == null)
+            _assetNames = sort(_inspectedComponent.getAssets().keySet());
+
+        return _assetNames;
+    }
+
+    public List getSortedComponents()
+    {
+        if (_sortedComponents != null)
+            return _sortedComponents;
+
+        Inspector inspector = (Inspector) getPage();
+        IComponent inspectedComponent = inspector.getInspectedComponent();
+
+        // Get a Map of the components and simply return null if there
+        // are none.
+
+        Map components = inspectedComponent.getComponents();
+
+        _sortedComponents = new ArrayList(components.values());
+
+        Collections.sort(_sortedComponents, new ComponentComparitor());
+
+        return _sortedComponents;
+    }
+
+    public void setComponent(IComponent value)
+    {
+        _component = value;
+    }
+
+    public IComponent getComponent()
+    {
+        return _component;
+    }
+
+    /**
+     *  Returns the type of the component, as specified in the container's
+     *  specification (i.e., the component alias if known).
+     *
+     **/
+
+    public String getComponentType()
+    {
+        IComponent container = _component.getContainer();
+
+        IComponentSpecification containerSpecification = container.getSpecification();
+
+        String id = _component.getId();
+        IContainedComponent contained = containerSpecification.getComponent(id);
+
+        // Temporary:  An implicit component will not be in the containing
+        // component's specification as a ContainedComponent.
+
+        if (contained == null)
+            return null;
+
+        return contained.getType();
+    }
+
+    /**
+     *  Returns a list of the properties for the component
+     *  (from its specification), or null if the component
+     *  has no properties.
+     *
+     **/
+
+    public List getSortedPropertyNames()
+    {
+        if (_sortedPropertyNames == null)
+            _sortedPropertyNames = sort(_inspectedSpecification.getPropertyNames());
+
+        return _sortedPropertyNames;
+    }
+
+    public void setPropertyName(String value)
+    {
+        _propertyName = value;
+    }
+
+    public String getPropertyName()
+    {
+        return _propertyName;
+    }
+
+    public String getPropertyValue()
+    {
+        return _inspectedSpecification.getProperty(_propertyName);
+    }
+
+    public List getBeanNames()
+    {
+        if (_beanNames == null)
+            _beanNames = sort(_inspectedSpecification.getBeanNames());
+
+        return _beanNames;
+    }
+
+    public void setBeanName(String value)
+    {
+        _beanName = value;
+        _beanSpecification = _inspectedSpecification.getBeanSpecification(_beanName);
+    }
+
+    public String getBeanName()
+    {
+        return _beanName;
+    }
+
+    public IBeanSpecification getBeanSpecification()
+    {
+        return _beanSpecification;
+    }
+
+    private List sort(Collection c)
+    {
+        if (c == null || c.size() == 0)
+            return null;
+
+        List result = new ArrayList(c);
+
+        Collections.sort(result);
+
+        return result;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowSpecification.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowSpecification.jwc
new file mode 100644
index 0000000..fa9c420
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowSpecification.jwc
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd" > 
+  
+<component-specification class="org.apache.tapestry.contrib.inspector.ShowSpecification">
+
+  <bean name="formalClass" class="org.apache.tapestry.bean.EvenOdd"/>           
+  <bean name="informalClass" class="org.apache.tapestry.bean.EvenOdd"/>
+  <bean name="assetClass" class="org.apache.tapestry.bean.EvenOdd"/>
+  <bean name="propertyClass" class="org.apache.tapestry.bean.EvenOdd"/>
+  <bean name="componentClass" class="org.apache.tapestry.bean.EvenOdd"/>
+  <bean name="beanClass" class="org.apache.tapestry.bean.EvenOdd"/>
+        
+  <component id="e_formal" type="Foreach">
+    <binding name="source" expression="formalParameterNames"/>
+    <binding name="value" expression="parameterName"/>
+    <static-binding name="element">tr</static-binding>
+    <binding name="class" expression="beans.formalClass.next"/>
+  </component>
+     
+  
+  <component id="e_informal" type="Foreach"> 
+    <binding name="source" expression="informalParameterNames"/>
+    <binding name="value" expression="parameterName"/>
+    <static-binding name="element">tr</static-binding>
+    <binding name="class" expression="beans.informalClass.next"/>
+  </component>
+
+  <component id="e_asset" type="Foreach">
+    <binding name="source" expression="assetNames"/>
+    <binding name="value" expression="assetName"/>
+    <static-binding name="element">tr</static-binding>
+    <binding name="class" expression="beans.assetClass.next"/>
+  </component>
+  
+  <component id="e_components" type="Foreach">
+    <binding name="source" expression="sortedComponents"/>
+    <binding name="value" expression="component"/>
+    <static-binding name="element">tr</static-binding>
+    <binding name="class" expression="beans.componentClass.next"/>
+  </component>
+
+  <component id="selectComponent" type="DirectLink">
+    <binding name="listener" expression="page.listeners.selectComponent"/>
+    <binding name="parameters" expression="component.idPath"/>
+  </component>
+
+  <component id="e_property" type="Foreach">
+    <binding name="source" expression="sortedPropertyNames"/>
+    <binding name="value" expression="propertyName"/>
+    <static-binding name="element">tr</static-binding>
+    <binding name="class" expression="beans.propertyClass.next"/>
+  </component>
+      
+  <component id="e_bean" type="Foreach">
+  	<binding name="source" expression="beanNames"/>
+  	<binding name="value" expression="beanName"/>
+  	<static-binding name="element">tr</static-binding>
+  	<binding name="class" expression="beans.beanClass.next"/>
+  </component>
+  	
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowTemplate.html b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowTemplate.html
new file mode 100644
index 0000000..2673368
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowTemplate.html
@@ -0,0 +1,15 @@
+<!-- $Id$ -->
+
+<span jwcid="ifNoTemplate">
+<span class="message">Component does not have a template.
+</span>
+</span>
+
+<span jwcid="ifTemplate">
+<table class="template">
+	<tr>
+		<td><span jwcid="insertTemplate"/></td>
+	</tr>
+</table>
+
+</span>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowTemplate.java b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowTemplate.java
new file mode 100644
index 0000000..65a8c13
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowTemplate.java
@@ -0,0 +1,333 @@
+//  Copyright 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.tapestry.contrib.inspector;
+
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IDirect;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.engine.IEngineService;
+import org.apache.tapestry.engine.ILink;
+import org.apache.tapestry.engine.ITemplateSource;
+import org.apache.tapestry.parse.CloseToken;
+import org.apache.tapestry.parse.ComponentTemplate;
+import org.apache.tapestry.parse.LocalizationToken;
+import org.apache.tapestry.parse.OpenToken;
+import org.apache.tapestry.parse.TemplateAttribute;
+import org.apache.tapestry.parse.TemplateToken;
+import org.apache.tapestry.parse.TextToken;
+import org.apache.tapestry.parse.TokenType;
+
+/**
+ *  Component of the {@link Inspector} page used to display
+ *  the ids and types of all embedded components.
+ *
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public class ShowTemplate extends BaseComponent implements IDirect
+{
+
+    public boolean getHasTemplate()
+    {
+        Inspector inspector;
+
+        inspector = (Inspector) getPage();
+
+        // Components that inherit from BaseComponent have templates,
+        // others do not.
+
+        return inspector.getInspectedComponent() instanceof BaseComponent;
+    }
+
+    public IRender getTemplateDelegate()
+    {
+        return new IRender()
+        {
+            public void render(IMarkupWriter writer, IRequestCycle cycle)
+            {
+                writeTemplate(writer, cycle);
+            }
+        };
+    }
+
+    /**
+     *  Writes the HTML template for the component.  When &lt;jwc&gt; tags are
+     *  written, the id is made a link (that selects the named component).  We
+     *  use some magic to accomplish this, creating links as if we were a
+     *  {@link DirectLink} component, and attributing those links
+     *  to the captive {@link DirectLink} component embedded here.
+     *
+     **/
+
+    private void writeTemplate(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        IComponent inspectedComponent = getInspectedComponent();
+        ComponentTemplate template = null;
+        ITemplateSource source = getPage().getEngine().getTemplateSource();
+
+        try
+        {
+            template = source.getTemplate(cycle, inspectedComponent);
+        }
+        catch (Exception ex)
+        {
+            return;
+        }
+
+        writer.begin("pre");
+
+        int count = template.getTokenCount();
+
+        for (int i = 0; i < count; i++)
+        {
+            TemplateToken token = template.getToken(i);
+            TokenType type = token.getType();
+
+            if (type == TokenType.TEXT)
+            {
+                write(writer, (TextToken) token);
+                continue;
+            }
+
+            if (type == TokenType.CLOSE)
+            {
+                write(writer, (CloseToken) token);
+
+                continue;
+            }
+
+            if (token.getType() == TokenType.LOCALIZATION)
+            {
+
+                write(writer, (LocalizationToken) token);
+                continue;
+            }
+
+            if (token.getType() == TokenType.OPEN)
+            {
+                boolean nextIsClose =
+                    (i + 1 < count) && (template.getToken(i + 1).getType() == TokenType.CLOSE);
+
+                write(writer, nextIsClose, (OpenToken) token);
+
+                if (nextIsClose)
+                    i++;
+
+                continue;
+            }
+
+            // That's all the types known at this time.
+        }
+
+        writer.end(); // <pre>        
+    }
+
+    /** @since 3.0 **/
+
+    private IComponent getInspectedComponent()
+    {
+        Inspector page = (Inspector) getPage();
+
+        return page.getInspectedComponent();
+    }
+
+    /** @since 3.0 **/
+
+    private void write(IMarkupWriter writer, TextToken token)
+    {
+        int start = token.getStartIndex();
+        int end = token.getEndIndex();
+
+        // Print the section of the template ... print() will
+        // escape and invalid characters as HTML entities.  Also,
+        // we show the full stretch of text, not the trimmed version.
+
+        writer.print(token.getTemplateData(), start, end - start + 1);
+    }
+
+    /** @since 3.0 **/
+
+    private void write(IMarkupWriter writer, CloseToken token)
+    {
+        writer.begin("span");
+        writer.attribute("class", "jwc-tag");
+
+        writer.print("</");
+        writer.print(token.getTag());
+        writer.print(">");
+
+        writer.end(); // <span>
+    }
+
+    /** @since 3.0 **/
+
+    private void write(IMarkupWriter writer, LocalizationToken token)
+    {
+        IComponent component = getInspectedComponent();
+
+        writer.begin("span");
+        writer.attribute("class", "jwc-tag");
+
+        writer.print("<span key=\"");
+        writer.print(token.getKey());
+        writer.print('"');
+
+        Map attributes = token.getAttributes();
+        if (attributes != null && !attributes.isEmpty())
+        {
+            Iterator it = attributes.entrySet().iterator();
+            while (it.hasNext())
+            {
+                Map.Entry entry = (Map.Entry) it.next();
+                String attributeName = (String) entry.getKey();
+                String attributeValue = (String) entry.getValue();
+
+                writer.print(' ');
+                writer.print(attributeName);
+                writer.print("=\"");
+                writer.print(attributeValue);
+                writer.print('"');
+
+            }
+        }
+
+        writer.print('>');
+        writer.begin("span");
+        writer.attribute("class", "localized-string");
+
+        writer.print(component.getMessages().getMessage(token.getKey()));
+        writer.end(); // <span>
+
+        writer.print("</span>");
+
+        writer.end(); // <span>
+    }
+
+    /** @since 3.0 **/
+
+    private void write(IMarkupWriter writer, boolean nextIsClose, OpenToken token)
+    {
+        IComponent component = getInspectedComponent();
+        IEngineService service = getPage().getEngine().getService(Tapestry.DIRECT_SERVICE);
+        String[] context = new String[1];
+
+        // Each id references a component embedded in the inspected component.
+        // Get that component.
+
+        String id = token.getId();
+        IComponent embedded = component.getComponent(id);
+        context[0] = embedded.getIdPath();
+
+        // Build a URL to select that component, as if by the captive
+        // component itself (it's a Direct).
+
+        ILink link = service.getLink(getPage().getRequestCycle(), this, context);
+
+        writer.begin("span");
+        writer.attribute("class", "jwc-tag");
+
+        writer.print("<");
+        writer.print(token.getTag());
+
+        writer.print(" jwcid=\"");
+
+        writer.begin("span");
+        writer.attribute("class", "jwc-id");
+
+        writer.begin("a");
+        writer.attribute("href", link.getURL());
+        writer.print(id);
+
+        writer.end(); // <a>
+        writer.end(); // <span>
+        writer.print('"');
+
+        Map attributes = token.getAttributesMap();
+
+        if (attributes != null)
+        {
+            Iterator ii = attributes.entrySet().iterator();
+
+            while (ii.hasNext())
+            {
+                Map.Entry e = (Map.Entry) ii.next();
+                
+                TemplateAttribute attribute = (TemplateAttribute)e.getValue();               
+                
+                writer.print(' ');
+                writer.print(e.getKey().toString());
+                writer.print("=\"");
+                
+                // TODO: Fix this to output something appropriate for each type
+                // of attribute (literal, expression, string).
+                
+                writer.print(attribute.getValue());
+                writer.print('"');
+            }
+        }
+
+        // Collapse an open & close down to a single tag.
+
+        if (nextIsClose)
+            writer.print('/');
+
+        writer.print('>');
+        writer.end(); // <span>
+    }
+
+    /**
+     *  Invoked when a component id is clicked.
+     *
+     **/
+
+    public void trigger(IRequestCycle cycle)
+    {
+        Inspector inspector = (Inspector) getPage();
+
+        Object[] parameters = cycle.getServiceParameters();
+
+        inspector.selectComponent((String) parameters[0]);
+
+        IComponent newComponent = inspector.getInspectedComponent();
+
+        // If the component is not a BaseComponent then it won't have
+        // a template, so switch to the specification view.
+
+        if (!(newComponent instanceof BaseComponent))
+            inspector.setView(View.SPECIFICATION);
+    }
+
+    /**
+     *  Always returns true.
+     * 
+     *  @since 2.3
+     * 
+     **/
+
+    public boolean isStateful()
+    {
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowTemplate.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowTemplate.jwc
new file mode 100644
index 0000000..266a467
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ShowTemplate.jwc
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.contrib.inspector.ShowTemplate">
+  <component id="ifNoTemplate" type="Conditional">
+    <binding name="condition" expression="! hasTemplate"/>
+  </component>
+  
+  <component id="ifTemplate" type="Conditional">
+    <binding name="condition" expression="hasTemplate"/>
+  </component>
+  
+  <component id="insertTemplate" type="Delegator">
+    <binding name="delegate" expression="templateDelegate"/>
+  </component>
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Specification_HRp4.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Specification_HRp4.gif
new file mode 100644
index 0000000..7b695d0
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Specification_HRp4.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Specification_Hp3.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Specification_Hp3.gif
new file mode 100644
index 0000000..33062cb
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Specification_Hp3.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Specification_NBanner.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Specification_NBanner.gif
new file mode 100644
index 0000000..896f81b
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Specification_NBanner.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Specification_NRp2.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Specification_NRp2.gif
new file mode 100644
index 0000000..6700d10
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Specification_NRp2.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Specification_Np1.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Specification_Np1.gif
new file mode 100644
index 0000000..5f8b7db
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Specification_Np1.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Template_HRp4.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Template_HRp4.gif
new file mode 100644
index 0000000..16e1792
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Template_HRp4.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Template_Hp3.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Template_Hp3.gif
new file mode 100644
index 0000000..2bc30ee
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Template_Hp3.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Template_NBanner.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Template_NBanner.gif
new file mode 100644
index 0000000..e250138
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Template_NBanner.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Template_NRp2.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Template_NRp2.gif
new file mode 100644
index 0000000..b7e2175
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Template_NRp2.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Template_Np1.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Template_Np1.gif
new file mode 100644
index 0000000..a83fb37
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/Template_Np1.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/View.java b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/View.java
new file mode 100644
index 0000000..6aac890
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/View.java
@@ -0,0 +1,68 @@
+//  Copyright 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.tapestry.contrib.inspector;
+
+import org.apache.commons.lang.enum.Enum;
+
+/**
+ *  Identifies different views for the inspector.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public class View extends Enum
+{
+    /**
+     *  View that displays the basic specification information, plus
+     *  formal and informal parameters (and related bindings), and 
+     *  assets.
+     *
+     **/
+
+    public static final View SPECIFICATION = new View("SPECIFICATION");
+
+    /**
+     *  View that displays the HTML template for the component, if one
+     *  exists.
+     *
+     **/
+
+    public static final View TEMPLATE = new View("TEMPLATE");
+
+    /**
+     *  View that shows the persistent properties of the page containing
+     *  the inspected component.
+     *
+     **/
+
+    public static final View PROPERTIES = new View("PROPERTIES");
+
+    /**
+     *  View that shows information about the 
+     *  {@link org.apache.tapestry.IEngine}.
+     *
+     **/
+
+    public static final View ENGINE = new View("ENGINE");
+
+
+    private View(String name)
+    {
+        super(name);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ViewTabs.html b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ViewTabs.html
new file mode 100644
index 0000000..6d3a970
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ViewTabs.html
@@ -0,0 +1,20 @@
+<!-- $Id$ -->
+
+<table border=0 cellpadding=0 cellspacing=2>
+	<tr>
+		<td>
+<span jwcid="@Foreach" source="ognl:views" value="ognl:view">
+			<a jwcid="select@ActionLink" listener="ognl:listeners.selectTab"><img 
+				jwcid="@Rollover" image="ognl:viewImage" focus="ognl:focusImage"
+				width="120" height="19"/></a>
+</span>
+		</td>
+	</tr>
+	<tr>
+		<td><img jwcid="@Image" image="ognl:bannerImage"/></td>
+	</tr>
+	<tr>
+		<td><span jwcid="@RenderBody"/></td>
+	</tr>
+</table>
+
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ViewTabs.java b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ViewTabs.java
new file mode 100644
index 0000000..3251d35
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ViewTabs.java
@@ -0,0 +1,92 @@
+//  Copyright 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.tapestry.contrib.inspector;
+
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.IAsset;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  Component of the {@link Inspector} page used to select the view.
+ *
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public abstract class ViewTabs extends BaseComponent
+{
+    private static View[] _views =
+        {
+            View.SPECIFICATION,
+            View.TEMPLATE,
+            View.PROPERTIES,
+            View.ENGINE };
+
+     public View[] getViews()
+    {
+        return _views;
+    }
+
+    public abstract void setView(View value);
+
+    public abstract View getView();
+
+    private IAsset getImageForView(boolean focus)
+    {
+        Inspector inspector = (Inspector) getPage();
+		View view = getView();
+		
+        boolean selected = (view == inspector.getView());
+
+        StringBuffer buffer = new StringBuffer(view.getName());
+
+        if (selected)
+            buffer.append("_selected");
+
+        if (focus)
+            buffer.append("_focus");
+
+        String key = buffer.toString();
+
+        return (IAsset) getAssets().get(key);
+    }
+
+    public IAsset getViewImage()
+    {
+        return getImageForView(false);
+    }
+
+    public IAsset getFocusImage()
+    {
+        return getImageForView(true);
+    }
+
+    public IAsset getBannerImage()
+    {
+         Inspector inspector = (Inspector) getPage();
+        View selectedView = inspector.getView();
+        String key = selectedView.getName() + "_banner";
+
+        return (IAsset) getAssets().get(key);
+    }
+
+    public void selectTab(IRequestCycle cycle)
+    {
+        Inspector inspector = (Inspector) getPage();
+        inspector.setView(getView());
+    }
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ViewTabs.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ViewTabs.jwc
new file mode 100644
index 0000000..1e03bad
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/ViewTabs.jwc
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.contrib.inspector.ViewTabs">
+  
+  <property-specification name="view" type="org.apache.tapestry.contrib.inspector.View"/>
+  
+  <private-asset name="SPECIFICATION" resource-path="Specification_Np1.gif"/>
+  <private-asset name="SPECIFICATION_selected" resource-path="Specification_Hp3.gif"/>
+  <private-asset name="SPECIFICATION_focus" resource-path="Specification_NRp2.gif"/>
+  <private-asset name="SPECIFICATION_selected_focus" resource-path="Specification_HRp4.gif"/>
+  <private-asset name="SPECIFICATION_banner" resource-path="Specification_NBanner.gif"/>
+  <private-asset name="TEMPLATE" resource-path="Template_Np1.gif"/>
+  <private-asset name="TEMPLATE_selected" resource-path="Template_Hp3.gif"/>
+  <private-asset name="TEMPLATE_focus" resource-path="Template_NRp2.gif"/>
+  <private-asset name="TEMPLATE_selected_focus" resource-path="Template_HRp4.gif"/>
+  <private-asset name="TEMPLATE_banner" resource-path="Template_NBanner.gif"/>
+  <private-asset name="PROPERTIES" resource-path="Properties_Np1.gif"/>
+  <private-asset name="PROPERTIES_selected" resource-path="Properties_Hp3.gif"/>
+  <private-asset name="PROPERTIES_focus" resource-path="Properties_NRp2.gif"/>
+  <private-asset name="PROPERTIES_selected_focus" resource-path="Properties_HRp4.gif"/>
+  <private-asset name="PROPERTIES_banner" resource-path="Properties_NBanner.gif"/>
+  <private-asset name="ENGINE" resource-path="Engine_Np1.gif"/>
+  <private-asset name="ENGINE_selected" resource-path="Engine_Hp3.gif"/>
+  <private-asset name="ENGINE_focus" resource-path="Engine_NRp2.gif"/>
+  <private-asset name="ENGINE_selected_focus" resource-path="Engine_HRp4.gif"/>
+  <private-asset name="ENGINE_banner" resource-path="Engine_NBanner.gif"/>
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/info.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/info.gif
new file mode 100644
index 0000000..be3d1d1
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/info.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/inspector-rollover.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/inspector-rollover.gif
new file mode 100644
index 0000000..df0d3ef
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/inspector-rollover.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/package.html b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/package.html
new file mode 100644
index 0000000..7610fdb
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/package.html
@@ -0,0 +1,17 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+<p>Implementation of the Tapestry <em>Inspector</em>, a specialized page
+used to dynamically introspect the construction of an application while
+it runs.  The {@link org.apache.tapestry.contrib.inspector.InspectorButton} component
+creates an icon on the page that raises the Inspector in a seperate window.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/tapestry-logo.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/tapestry-logo.gif
new file mode 100644
index 0000000..82ad636
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/inspector/tapestry-logo.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/BooleanParameter.java b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/BooleanParameter.java
new file mode 100644
index 0000000..80fc31d
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/BooleanParameter.java
@@ -0,0 +1,57 @@
+//  Copyright 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.tapestry.contrib.jdbc;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+/**
+ *  Wrapper around a boolean parameter.
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.3
+ *
+ **/
+
+public class BooleanParameter implements IParameter
+{
+    private boolean _value;
+
+    public static final BooleanParameter TRUE = new BooleanParameter(true);
+
+    public static final BooleanParameter FALSE = new BooleanParameter(false);
+
+    private BooleanParameter(boolean value)
+    {
+        _value = value;
+    }
+
+    public void set(PreparedStatement statement, int index) throws SQLException
+    {
+        statement.setBoolean(index, _value);
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer("Boolean<");
+        buffer.append(_value);
+        buffer.append('>');
+
+        return buffer.toString();
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/DoubleParameter.java b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/DoubleParameter.java
new file mode 100644
index 0000000..e9f6d65
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/DoubleParameter.java
@@ -0,0 +1,52 @@
+//  Copyright 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.tapestry.contrib.jdbc;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+/**
+ *  A wrapper around a double parameter.
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.3
+ * 
+ **/
+
+public class DoubleParameter implements IParameter
+{
+    private double _value;
+
+    public DoubleParameter(double value)
+    {
+        _value = value;
+    }
+
+    public void set(PreparedStatement statement, int index) throws SQLException
+    {
+        statement.setDouble(index, _value);
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer("Double<");
+        buffer.append(_value);
+        buffer.append('>');
+
+        return buffer.toString();
+    }
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/FloatParameter.java b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/FloatParameter.java
new file mode 100644
index 0000000..b2eb348
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/FloatParameter.java
@@ -0,0 +1,52 @@
+//  Copyright 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.tapestry.contrib.jdbc;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+/**
+ *  A wrapper around a float parameter.
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.3
+ * 
+ **/
+
+public class FloatParameter implements IParameter
+{
+    private float _value;
+
+    public FloatParameter(float value)
+    {
+        _value = value;
+    }
+
+    public void set(PreparedStatement statement, int index) throws SQLException
+    {
+        statement.setFloat(index, _value);
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer("Float<");
+        buffer.append(_value);
+        buffer.append('>');
+
+        return buffer.toString();
+    }
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/IParameter.java b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/IParameter.java
new file mode 100644
index 0000000..2032a00
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/IParameter.java
@@ -0,0 +1,41 @@
+//  Copyright 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.tapestry.contrib.jdbc;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+/**
+ *  Represents a parameter within a dynamically generated SQL statement.
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.3
+ *  @see org.apache.tapestry.contrib.jdbc.ParameterizedStatement
+ * 
+ **/
+
+public interface IParameter
+{
+    /**
+     *  Invokes the appropriate setXXX() method on the 
+     *  {@link java.sql.PreparedStatement}.
+     * 
+     **/
+    
+    public void set(PreparedStatement statement, int index)
+    throws SQLException;
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/IStatement.java b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/IStatement.java
new file mode 100644
index 0000000..c9f2544
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/IStatement.java
@@ -0,0 +1,70 @@
+//  Copyright 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.tapestry.contrib.jdbc;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+/**
+ *  A wrapper around {@link java.sql.Statement} or 
+ *  {@link java.sql.PreparedStatement} which hides the differences
+ *  between the two.  
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *  @see org.apache.tapestry.contrib.jdbc.StatementAssembly#createStatement(Connection)
+ * 
+ **/
+
+public interface IStatement
+{
+    /**
+     * Returns the SQL associated with this statement.
+     *
+     **/
+
+    public String getSQL();
+
+    /**
+     *  Returns the underlying {@link java.sql.Statement} 
+     *  (or {@link java.sql.PreparedStatement}).
+     *
+     **/
+
+    public Statement getStatement();
+
+    /**
+     *  Closes the underlying statement, and nulls the reference to it.
+     *
+     **/
+
+    public void close() throws SQLException;
+
+    /**
+     *  Executes the statement as a query, returning a {@link ResultSet}.
+     *
+     **/
+
+    public ResultSet executeQuery() throws SQLException;
+
+    /**
+     *  Executes the statement as an update, returning the number of rows
+     *  affected.
+     *
+     **/
+
+    public int executeUpdate() throws SQLException;
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/IntegerParameter.java b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/IntegerParameter.java
new file mode 100644
index 0000000..9148201
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/IntegerParameter.java
@@ -0,0 +1,52 @@
+//  Copyright 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.tapestry.contrib.jdbc;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+/**
+ *  A wrapper around an integer parameter.
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.3
+ * 
+ **/
+
+public class IntegerParameter implements IParameter
+{
+    private int _value;
+
+    public IntegerParameter(int value)
+    {
+        _value = value;
+    }
+
+    public void set(PreparedStatement statement, int index) throws SQLException
+    {
+        statement.setInt(index, _value);
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer("Integer<");
+        buffer.append(_value);
+        buffer.append('>');
+
+        return buffer.toString();
+    }
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/LongParameter.java b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/LongParameter.java
new file mode 100644
index 0000000..847e8a0
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/LongParameter.java
@@ -0,0 +1,52 @@
+//  Copyright 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.tapestry.contrib.jdbc;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+/**
+ *  Wrapper around long parameter.
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.3
+ * 
+ **/
+
+public class LongParameter implements IParameter
+{
+    private long _value;
+
+    public LongParameter(long value)
+    {
+        _value = value;
+    }
+
+    public void set(PreparedStatement statement, int index) throws SQLException
+    {
+        statement.setLong(index, _value);
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer("Long<");
+        buffer.append(_value);
+        buffer.append('>');
+
+        return buffer.toString();
+    }
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/ObjectParameter.java b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/ObjectParameter.java
new file mode 100644
index 0000000..25be80f
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/ObjectParameter.java
@@ -0,0 +1,52 @@
+//  Copyright 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.tapestry.contrib.jdbc;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+/**
+ *  An arbitrary object parameter.
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.3
+ * 
+ **/
+
+public class ObjectParameter implements IParameter
+{
+    private Object _value;
+
+    public ObjectParameter(Object value)
+    {
+        _value = value;
+    }
+
+    public void set(PreparedStatement statement, int index) throws SQLException
+    {
+        statement.setObject(index, _value);
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer("Object<");
+        buffer.append(_value);
+        buffer.append('>');
+
+        return buffer.toString();
+    }
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/ParameterizedStatement.java b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/ParameterizedStatement.java
new file mode 100644
index 0000000..3d37cca
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/ParameterizedStatement.java
@@ -0,0 +1,152 @@
+//  Copyright 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.tapestry.contrib.jdbc;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.List;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ *  A wrapper around {@link PreparedStatement}.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ * 
+ **/
+
+public class ParameterizedStatement implements IStatement
+{
+    private static final Log LOG = LogFactory.getLog(ParameterizedStatement.class);
+
+    private String _SQL;
+    private PreparedStatement _statement;
+    private IParameter[] _parameters;
+
+    /**
+     *  Create a new instance; the parameters list is copied.
+     * 
+     *  @param SQL the SQL to execute (see {@link Connection#prepareStatement(java.lang.String)})
+     *  @param connection the JDBC connection to use
+     *  @param parameters list of {@link IParameter}
+     * 
+     **/
+    
+    public ParameterizedStatement(String SQL, Connection connection, List parameters) throws SQLException
+    {
+        _SQL = SQL;
+
+        _statement = connection.prepareStatement(SQL);
+
+        _parameters = (IParameter[]) parameters.toArray(new IParameter[parameters.size()]);
+
+        for (int i = 0; i < _parameters.length; i++)
+        {
+            // JDBC numbers things from 1, not 0.
+
+            _parameters[i].set(_statement, i + 1);
+        }
+    }
+
+    /**
+     * Returns the SQL associated with this statement.
+     *
+     **/
+
+    public String getSQL()
+    {
+        return _SQL;
+    }
+
+    /**
+     *  Returns the underlying or {@link PreparedStatement}.
+     *
+     **/
+
+    public Statement getStatement()
+    {
+        return _statement;
+    }
+
+    /**
+     *  Closes the underlying statement, and nulls the reference to it.
+     *
+     **/
+
+    public void close() throws SQLException
+    {
+        _statement.close();
+
+        _statement = null;
+        _SQL = null;
+    }
+
+    /**
+     *  Executes the statement as a query, returning a {@link ResultSet}.
+     *
+     **/
+
+    public ResultSet executeQuery() throws SQLException
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Executing query: " + this);
+
+        return _statement.executeQuery();
+    }
+
+    /**
+     *  Executes the statement as an update, returning the number of rows
+     *  affected.
+     *
+     **/
+
+    public int executeUpdate() throws SQLException
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Executing update: " + this);
+
+        return _statement.executeUpdate();
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer("ParameterizedStatement@");
+        buffer.append(Integer.toHexString(hashCode()));
+        buffer.append("[SQL=\n<");
+        buffer.append(_SQL);
+        buffer.append("\n>");
+
+        for (int i = 0; i < _parameters.length; i++)
+        {
+            IParameter parameter = _parameters[i];
+
+            buffer.append(" ?");
+            buffer.append(i + 1);
+            buffer.append('=');
+
+            buffer.append(parameter);
+        }
+
+        buffer.append(']');
+
+        return buffer.toString();
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/ShortParameter.java b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/ShortParameter.java
new file mode 100644
index 0000000..31cfea7
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/ShortParameter.java
@@ -0,0 +1,53 @@
+//  Copyright 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.tapestry.contrib.jdbc;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+/**
+ *  A wrapper around a short parameter.  
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.3
+ * 
+ **/
+
+public class ShortParameter implements IParameter
+{
+    private short _value;
+
+    public ShortParameter(short value)
+    {
+        _value = value;
+    }
+
+    public void set(PreparedStatement statement, int index) throws SQLException
+    {
+        statement.setShort(index, _value);
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer("Short<");
+        buffer.append(_value);
+        buffer.append('>');
+
+        return buffer.toString();
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/SimpleStatement.java b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/SimpleStatement.java
new file mode 100644
index 0000000..16df20f
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/SimpleStatement.java
@@ -0,0 +1,127 @@
+//  Copyright 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.tapestry.contrib.jdbc;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ *  A wrapper around {@link Statement}.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public class SimpleStatement implements IStatement
+{
+    private static final Log LOG = LogFactory.getLog(SimpleStatement.class);
+
+    private String _sql;
+    private Statement _statement;
+
+    public SimpleStatement(String SQL, Connection connection) throws SQLException
+    {
+        _sql = SQL;
+        _statement = connection.createStatement();
+    }
+
+    public SimpleStatement(String SQL, Connection connection, int resultSetType, int resultSetConcurrency)
+        throws SQLException
+    {
+        _sql = SQL;
+        _statement = connection.createStatement(resultSetType, resultSetConcurrency);
+    }
+
+    /**
+     * Returns the SQL associated with this statement.
+     *
+     **/
+
+    public String getSQL()
+    {
+        return _sql;
+    }
+
+    /**
+     *  Returns the underlying {@link Statement}.
+     *
+     **/
+
+    public Statement getStatement()
+    {
+        return _statement;
+    }
+
+    /**
+     *  Closes the underlying statement, and nulls the reference to it.
+     *
+     **/
+
+    public void close() throws SQLException
+    {
+        _statement.close();
+
+        _statement = null;
+        _sql = null;
+    }
+
+    /**
+     *  Executes the statement as a query, returning a {@link ResultSet}.
+     *
+     **/
+
+    public ResultSet executeQuery() throws SQLException
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Executing query: " + this);
+
+        return _statement.executeQuery(_sql);
+    }
+
+    /**
+     *  Executes the statement as an update, returning the number of rows
+     *  affected.
+     *
+     **/
+
+    public int executeUpdate() throws SQLException
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Executing update: " + this);
+
+        return _statement.executeUpdate(_sql);
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer;
+
+        buffer = new StringBuffer("SimpleStatement@");
+        buffer.append(Integer.toHexString(hashCode()));
+
+        buffer.append("[SQL=<\n");
+        buffer.append(_sql);
+        buffer.append("\n>]");
+
+        return buffer.toString();
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/StatementAssembly.java b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/StatementAssembly.java
new file mode 100644
index 0000000..112e740
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/StatementAssembly.java
@@ -0,0 +1,480 @@
+//  Copyright 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.tapestry.contrib.jdbc;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.List;
+
+/**
+ *  Class for creating and executing JDBC statements.  Allows statements to be assembled
+ *  incrementally (like a {@link StringBuffer}), but also tracks parameters, shielding
+ *  the developer from the differences between constructing and 
+ *  using a JDBC 
+ *  {@link java.sql.Statement} and 
+ *  a JDBC {@link java.sql.PreparedStatement}.  This class is somewhat skewed towards
+ *  Oracle, which works more efficiently with prepared staments than
+ *  simple SQL.
+ * 
+ *  <p>In addition, implements {@link #toString()} in a useful way (you can see the
+ *  SQL and parameters), which is invaluable when debugging.
+ * 
+ *  <p>{@link #addParameter(int)} (and all overloaded versions of it for scalar types)
+ *  adds a "?" to the statement and records the parameter value.
+ * 
+ *  <p>{@link #addParameter(Integer)} (and all overloaded version of it for wrapper
+ *  types) does the same ... unless the value is null, in which case "NULL" is
+ *  inserted into the statement.
+ * 
+ *  <p>{@link #addParameterList(int[], String)} (and all overloaded versions of it)
+ *  simply invokes the appropriate {@link #addParameter(int)}, adding the
+ *  separator in between parameters.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public class StatementAssembly
+{
+    private StringBuffer _buffer = new StringBuffer();
+
+    private static final String NULL = "NULL";
+
+    public static final String SEP = ", ";
+
+    /**
+     *  List of {@link IParameter}
+     * 
+     **/
+
+    private List _parameters;
+
+    private int _lineLength;
+    private int _maxLineLength = 80;
+    private int _indent = 5;
+
+    /**
+     *  Default constructor; uses a maximum line length of 80 and an indent of 5.
+     *
+     **/
+
+    public StatementAssembly()
+    {
+    }
+
+    public StatementAssembly(int maxLineLength, int indent)
+    {
+        _maxLineLength = maxLineLength;
+        _indent = indent;
+    }
+
+    /**
+     *  Clears the assembly, preparing it for re-use.
+     * 
+     *  @since 1.0.7
+     * 
+     **/
+
+    public void clear()
+    {
+        _buffer.setLength(0);
+        _lineLength = 0;
+
+        if (_parameters != null)
+            _parameters.clear();
+    }
+
+    /**
+     *  Maximum length of a line.
+     *
+     **/
+
+    public int getMaxLineLength()
+    {
+        return _maxLineLength;
+    }
+
+    /**
+     *  Number of spaces to indent continuation lines by.
+     *
+     **/
+
+    public int getIndent()
+    {
+        return _indent;
+    }
+
+    /**
+     *  Adds text to the current line, unless that would make the line too long, in
+     *  which case a new line is started (and indented) before adding the text.
+     *
+     *  <p>Text is added as-is, with no concept of quoting.  To add arbitrary strings
+     *  (such as in a where clause), use {@link #addParameter(String)}.
+     *
+     *
+     **/
+
+    public void add(String text)
+    {
+        int textLength;
+
+        textLength = text.length();
+
+        if (_lineLength + textLength > _maxLineLength)
+        {
+            _buffer.append('\n');
+
+            for (int i = 0; i < _indent; i++)
+                _buffer.append(' ');
+
+            _lineLength = _indent;
+        }
+
+        _buffer.append(text);
+        _lineLength += textLength;
+    }
+    
+    public void add(short value)
+    {
+        add(Short.toString(value));
+    }
+    
+    public void add(int value)
+    {
+        add(Integer.toString(value));
+    }
+    
+    public void add(long value)
+    {
+        add(Long.toString(value));
+    }
+    
+    public void add(float value)
+    {
+        add(Float.toString(value));
+    }
+    
+    public void add(double value)
+    {
+        add(Double.toString(value));
+    }
+
+    /**
+     *  Adds a date value to a {@link StatementAssembly} converting
+     *  it to a {@link java.sql.Timestamp} first.
+     *
+     **/
+
+    public void addParameter(Date date)
+    {
+        if (date == null)
+        {
+            add("NULL");
+            return;
+        }
+
+        Calendar calendar = GregorianCalendar.getInstance();
+
+        calendar.setTime(date);
+        calendar.set(Calendar.MILLISECOND, 0);
+
+        Date adjusted = calendar.getTime();
+
+        Timestamp timestamp = new Timestamp(adjusted.getTime());
+
+        addParameter(timestamp);
+    }
+
+    /** 
+     *  Adds a separator (usually a comma and a space) to the current line, regardless
+     *  of line length.  This is purely aesthetic ... it just looks odd if a separator
+     *  gets wrapped to a new line by itself.
+     *
+     **/
+
+    public void addSep(String text)
+    {
+        _buffer.append(text);
+        _lineLength += text.length();
+    }
+
+    /**
+     *  Starts a new line, without indenting.
+     *
+     **/
+
+    public void newLine()
+    {
+        if (_buffer.length() != 0)
+            _buffer.append('\n');
+
+        _lineLength = 0;
+    }
+
+    /**
+     * Starts a new line, then adds the given text.
+     *
+     **/
+
+    public void newLine(String text)
+    {
+        if (_buffer.length() != 0)
+            _buffer.append('\n');
+
+        _buffer.append(text);
+
+        _lineLength = text.length();
+    }
+
+    public void addList(String[] items, String separator)
+    {
+        for (int i = 0; i < items.length; i++)
+        {
+            if (i > 0)
+                addSep(separator);
+
+            add(items[i]);
+        }
+    }
+
+    public void addParameterList(int[] items, String separator)
+    {
+        for (int i = 0; i < items.length; i++)
+        {
+            if (i > 0)
+                addSep(separator);
+
+            addParameter(items[i]);
+        }
+    }
+
+    public void addParameterList(Integer[] items, String separator)
+    {
+        for (int i = 0; i < items.length; i++)
+        {
+            if (i > 0)
+                addSep(separator);
+
+            addParameter(items[i]);
+        }
+    }
+
+    public void addParameterList(long[] items, String separator)
+    {
+        for (int i = 0; i < items.length; i++)
+        {
+            if (i > 0)
+                addSep(separator);
+
+            addParameter(items[i]);
+        }
+    }
+
+    public void addParameterList(Long[] items, String separator)
+    {
+        for (int i = 0; i < items.length; i++)
+        {
+            if (i > 0)
+                addSep(separator);
+
+            addParameter(items[i]);
+        }
+    }
+
+    public void addParameterList(String[] items, String separator)
+    {
+        for (int i = 0; i < items.length; i++)
+        {
+            if (i > 0)
+                addSep(separator);
+
+            addParameter(items[i]);
+        }
+    }
+
+    public void addParameterList(double[] items, String separator)
+    {
+        for (int i = 0; i < items.length; i++)
+        {
+            if (i > 0)
+                addSep(separator);
+
+            addParameter(items[i]);
+        }
+    }
+
+    public void addParameter(Object value)
+    {
+        if (value == null)
+            add(NULL);
+        else
+            addParameter(new ObjectParameter(value));
+    }
+
+    public void addParameter(Timestamp timestamp)
+    {
+        if (timestamp == null)
+            add(NULL);
+        else
+            addParameter(new TimestampParameter(timestamp));
+    }
+
+    public void addParameter(String value)
+    {
+        if (value == null)
+            add(NULL);
+        else
+            addParameter(new StringParameter(value));
+    }
+
+    public void addParameter(int value)
+    {
+        addParameter(new IntegerParameter(value));
+    }
+
+    public void addParameter(Integer value)
+    {
+        if (value == null)
+            add(NULL);
+        else
+            addParameter(value.intValue());
+    }
+
+    public void addParameter(long value)
+    {
+        addParameter(new LongParameter(value));
+    }
+
+    public void addParameter(Long value)
+    {
+        if (value == null)
+            add(NULL);
+        else
+            addParameter(value.longValue());
+    }
+
+    public void addParameter(float value)
+    {
+        addParameter(new FloatParameter(value));
+    }
+
+    public void addParameter(Float value)
+    {
+        if (value == null)
+            add(NULL);
+        else
+            addParameter(value.floatValue());
+    }
+
+    public void addParameter(double value)
+    {
+        addParameter(new DoubleParameter(value));
+    }
+
+    public void addParameter(Double value)
+    {
+        if (value == null)
+            add(NULL);
+        else
+            addParameter(value.doubleValue());
+    }
+
+    public void addParameter(short value)
+    {
+        addParameter(new ShortParameter(value));
+    }
+
+    public void addParameter(Short value)
+    {
+        if (value == null)
+            add(NULL);
+        else
+            addParameter(value.shortValue());
+    }
+
+    public void addParameter(boolean value)
+    {
+        addParameter(value ? BooleanParameter.TRUE : BooleanParameter.FALSE);
+    }
+
+    public void addParameter(Boolean value)
+    {
+        if (value == null)
+            add(NULL);
+        else
+            addParameter(value.booleanValue());
+    }
+
+    private void addParameter(IParameter parameter)
+    {
+        if (_parameters == null)
+            _parameters = new ArrayList();
+
+        _parameters.add(parameter);
+
+        add("?");
+    }
+
+    /**
+     *  Creates and returns an {@link IStatement} based on the SQL and parameters
+     *  acquired.
+     *
+     **/
+
+    public IStatement createStatement(Connection connection) throws SQLException
+    {
+        String sql = _buffer.toString();
+
+        if (_parameters == null || _parameters.isEmpty())
+            return new SimpleStatement(sql, connection);
+
+        return new ParameterizedStatement(sql, connection, _parameters);
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer("StatementAssembly@");
+
+        buffer.append(Integer.toHexString(hashCode()));
+        buffer.append("[SQL=\n<");
+        buffer.append(_buffer);
+        buffer.append("\n>");
+
+        if (_parameters != null)
+        {
+            int count = _parameters.size();
+            for (int i = 0; i < count; i++)
+            {
+                Object parameter = _parameters.get(i);
+
+                buffer.append(" ?");
+                buffer.append(i + 1);
+                buffer.append('=');
+
+                buffer.append(parameter);
+            }
+        }
+
+        buffer.append(']');
+
+        return buffer.toString();
+    }
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/StringParameter.java b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/StringParameter.java
new file mode 100644
index 0000000..088a2d7
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/StringParameter.java
@@ -0,0 +1,55 @@
+//  Copyright 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.tapestry.contrib.jdbc;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Types;
+
+/**
+ *  Used with String parameters.
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+public class StringParameter implements IParameter
+{
+    private String _value;
+
+    public StringParameter(String value)
+    {
+        _value = value;
+    }
+
+    public void set(PreparedStatement statement, int index) throws SQLException
+    {
+        if (_value == null)
+            statement.setNull(index, Types.VARCHAR);
+        else
+            statement.setString(index, _value);
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer("String<");
+        buffer.append(_value);
+        buffer.append('>');
+
+        return buffer.toString();
+    }
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/TimestampParameter.java b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/TimestampParameter.java
new file mode 100644
index 0000000..c5bd8fc
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/TimestampParameter.java
@@ -0,0 +1,56 @@
+//  Copyright 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.tapestry.contrib.jdbc;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.sql.Types;
+
+/**
+ *  Used with Timestamp parameters.
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+public class TimestampParameter implements IParameter
+{
+    private Timestamp _timestamp;
+
+    public TimestampParameter(Timestamp timestamp)
+    {
+        _timestamp = timestamp;
+    }
+
+    public void set(PreparedStatement statement, int index) throws SQLException
+    {
+        if (_timestamp == null)
+            statement.setNull(index, Types.TIMESTAMP);
+        else
+            statement.setTimestamp(index, _timestamp);
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer("Timestamp<");
+        buffer.append(_timestamp);
+        buffer.append('>');
+
+        return buffer.toString();
+    }
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/package.html b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/package.html
new file mode 100644
index 0000000..13983ad
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/jdbc/package.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+
+
+<body>
+
+<p>A set of classes that assist with dynamically generating JDBC SQL queries.  Importantly,
+they help hide the difference between a {@link java.sql.Statement} and
+{@link java.sql.PreparedStatement} ... in fact, using a
+{@link org.apache.tapestry.contrib.jdbc.StatementAssembly} you don't know in advance which
+you'll get, which is very handy when generating truly dynamic SQL.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/link/AreaLinkRenderer.java b/tapestry-contrib/src/org/apache/tapestry/contrib/link/AreaLinkRenderer.java
new file mode 100644
index 0000000..d27f12a
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/link/AreaLinkRenderer.java
@@ -0,0 +1,46 @@
+//  Copyright 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.tapestry.contrib.link;
+
+import org.apache.tapestry.link.DefaultLinkRenderer;
+import org.apache.tapestry.link.ILinkRenderer;
+
+/**
+ *  A subclass of {@link org.apache.tapestry.link.DefaultLinkRenderer} for
+ *  the HTML area element.
+ *
+ *  @author David Solis
+ *  @version $Id$
+ *  @since 3.0
+ **/
+public class AreaLinkRenderer extends DefaultLinkRenderer 
+{
+
+	/**
+	 *  A singleton for the area link. 
+	 **/
+
+	public static final ILinkRenderer SHARED_INSTANCE = new AreaLinkRenderer();
+
+	public String getElement() 
+	{
+		return "area";
+	}
+
+	public boolean getHasBody() 
+	{
+		return false;
+	}
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/link/PopupLinkRenderer.java b/tapestry-contrib/src/org/apache/tapestry/contrib/link/PopupLinkRenderer.java
new file mode 100644
index 0000000..b079b6d
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/link/PopupLinkRenderer.java
@@ -0,0 +1,88 @@
+//  Copyright 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.tapestry.contrib.link;
+
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.engine.ILink;
+import org.apache.tapestry.link.DefaultLinkRenderer;
+
+/**
+ *  This renderer emits javascript to launch the link in a window.
+ *
+ *  @author David Solis
+ *  @version $Id$
+ *  @since 3.0.1
+ **/
+public class PopupLinkRenderer extends DefaultLinkRenderer
+{
+
+	private String _windowName;
+
+	private String _features;
+
+	public PopupLinkRenderer()
+	{
+	}
+
+	/**
+	 * Initializes the name and features for javascript window.open function.
+	 *
+	 * @param windowName the window name
+	 * @param features   the window features
+	 */
+	public PopupLinkRenderer(String windowName, String features)
+	{
+		_windowName = windowName;
+		_features = features;
+	}
+
+	/**
+	 * @see DefaultLinkRenderer#constructURL(org.apache.tapestry.engine.ILink, String, org.apache.tapestry.IRequestCycle)
+	 */
+	protected String constructURL(ILink link, String anchor, IRequestCycle cycle)
+	{
+      if (cycle.isRewinding()) {
+        return null;
+      }
+      
+		String url = link.getURL(anchor, true);
+		return "javascript: w = window.open(" + normalizeString(url) + ", " + normalizeString(getWindowName()) + ", " + normalizeString(getFeatures()) + "); w.focus();";
+	}
+
+	private String normalizeString(String str)
+	{
+		return str == null ? "''" : "'" + str + "'";
+	}
+
+	public String getWindowName()
+	{
+		return _windowName;
+	}
+
+	public void setWindowName(String windowName)
+	{
+		_windowName = windowName;
+	}
+
+	public String getFeatures()
+	{
+		return _features;
+	}
+
+	public void setFeatures(String features)
+	{
+		_features = features;
+	}
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/palette/Palette.html b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/Palette.html
new file mode 100644
index 0000000..85e5095
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/Palette.html
@@ -0,0 +1,50 @@
+<!-- $Id$ -->
+
+<span jwcid="$content$">
+<table jwcid="@Any" element="table" class="ognl:tableClass">
+  <tr>
+  	<th class="available-header">
+ <span jwcid="@RenderBlock" block="ognl:availableTitleBlock"/>
+ <span jwcid="defaultAvailableTitleBlock@Block"><span key="title.available"/></span>
+</th>
+  	<td element="td" class="controls" rowspan="2">
+  	    <a jwcid="@Any" 
+  	    	element="a" 
+  	    	href="ognl:symbols.selectOnClickScript"><img jwcid="@Image"
+  	    		image="ognl:selectImage"
+  	    		name="ognl:symbols.selectImageName"
+  	    		alt="message:tooltip.select"/></a>
+
+    <a jwcid="@Any" element="a"
+    	href="ognl:symbols.deselectOnClickScript"><img jwcid="@Image"
+    		image="ognl:deselectImage"
+    		name="ognl:symbols.deselectImageName"
+    		alt="message:tooltip.deselect"/></a>
+    
+    <span jwcid="@Conditional" condition="ognl:sortUser">
+
+    <a jwcid="@Any" element="a"
+    	href="ognl:symbols.upOnClickScript"><img jwcid="@Image" 
+    		image="ognl:upImage"
+    		name="ognl:symbols.upImageName"
+    		alt="message:tooltip.moveup"/></a>
+
+    <a jwcid="@Any" element="a"
+    	href="ognl:symbols.downOnClickScript"><img jwcid="@Image"
+    		image="ognl:downImage"
+    		name="ognl:symbols.downImageName"
+    		alt="message:tooltip.movedown"/></a>
+    </span>
+
+    </td>
+  	<th class="selected-header">
+ <span jwcid="@RenderBlock" block="ognl:selectedTitleBlock"/>
+ <span jwcid="defaultSelectedTitleBlock@Block"><span key="title.selected"/></span>
+    </th>
+  </tr>
+  <tr>
+    <td class="available-cell"><select jwcid="@Delegator" delegate="ognl:availableColumn"/></td>
+    <td class="selected-cell"><select jwcid="@Delegator" delegate="ognl:selectedColumn"/></td>
+  </tr>
+</table>
+</span>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/palette/Palette.java b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/Palette.java
new file mode 100644
index 0000000..330c312
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/Palette.java
@@ -0,0 +1,583 @@
+//  Copyright 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.tapestry.contrib.palette;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.IAsset;
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.IScript;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.components.Block;
+import org.apache.tapestry.engine.IScriptSource;
+import org.apache.tapestry.form.Form;
+import org.apache.tapestry.form.FormEventType;
+import org.apache.tapestry.form.IFormComponent;
+import org.apache.tapestry.form.IPropertySelectionModel;
+import org.apache.tapestry.html.Body;
+import org.apache.tapestry.request.RequestContext;
+import org.apache.tapestry.valid.IValidationDelegate;
+
+/**
+ *  A component used to make a number of selections from a list.  The general look
+ *  is a pair of &lt;select&gt; elements.  with a pair of buttons between them.
+ *  The right element is a list of values that can be selected.  The buttons move
+ *  values from the right column ("available") to the left column ("selected").
+ *
+ *  <p>This all takes a bit of JavaScript to accomplish (quite a bit), which means
+ *  a {@link Body} component must wrap the Palette. If JavaScript is not enabled
+ *  in the client browser, then the user will be unable to make (or change) any selections.
+ *
+ *  <p>Cross-browser compatibility is not perfect.  In some cases, the 
+ *  {@link org.apache.tapestry.contrib.form.MultiplePropertySelection} component
+ *  may be a better choice.
+ * 
+ *  <p><table border=1>
+ * <tr>
+ *    <td>Parameter</td>
+ *    <td>Type</td>
+ *    <td>Direction </td>
+ *    <td>Required</td>
+ *    <td>Default</td>
+ *    <td>Description</td>
+ * </tr>
+ *
+ *  <tr>
+ *    <td>selected</td>
+ *  <td>{@link List}</td>
+ *  <td>in</td>
+ *  <td>yes</td>
+ *  <td>&nbsp;</td>
+ *  <td>A List of selected values.  Possible selections are defined by the model; this
+ *  should be a subset of the possible values.  This may be null when the
+ *  component is renderred.  When the containing form is submitted,
+ *  this parameter is updated with a new List of selected objects.
+ *
+ *  <p>The order may be set by the user, as well, depending on the
+ *  sortMode parameter.</td> </tr>
+ *
+ * <tr>
+ * <td>model</td>
+ *  <td>{@link IPropertySelectionModel}</td>
+ *  <td>in</td>
+ *  <td>yes</td>
+ *  <td>&nbsp;</td>
+ *  <td>Works, as with a {@link org.apache.tapestry.form.PropertySelection} component, to define the
+ *  possible values.
+ *  </td> </tr>
+ *
+ *  <tr>
+ *  <td>sort</td> 
+ *  <td>{@link SortMode}</td> 
+ *  <td>in</td>
+ *  <td>no</td> 
+ *  <td>{@link SortMode#NONE}</td>
+ *  <td>
+ *  Controls automatic sorting of the options. </td>
+ *  </tr>
+ *
+ * <tr>
+ *  <td>rows</td>
+ *  <td>int</td> 
+ *  <td>in</td> 
+ *  <td>no</td> 
+ *  <td>10</td>
+ *  <td>The number of rows that should be visible in the Pallete's &lt;select&gt;
+ *  elements.
+ *  </td> </tr>
+ *
+ * <tr>
+ *  <td>tableClass</td>
+ *  <td>{@link String}</td> 
+ *  <td>in</td>
+ *  <td>no</td> 
+ *  <td>tapestry-palette</td>
+ *  <td>The CSS class for the table which surrounds the other elements of
+ *  the Palette.</td> </tr>
+ *
+ * <tr>
+ *  <td>selectedTitleBlock</td>
+ *  <td>{@link Block}</td>
+ *  <td>in</td> 
+ *  <td>no</td> 
+ *  <td>"Selected"</td>
+ *  <td>If specified, allows a {@link Block} to be placed within
+ *  the &lt;th&gt; reserved for the title above the selected items
+ *  &lt;select&gt; (on the right).  This allows for images or other components to
+ *  be placed there.  By default, the simple word <code>Selected</code>
+ *  is used.</td> </tr>
+ *
+ * <tr>
+ *  <td>availableTitleBlock</td>
+ *  <td>{@link Block}</td>
+ *  <td>in</td> 
+ *  <td>no</td> 
+ *  <td>"Available"</td>
+ *  <td>As with selectedTitleBlock, but for the left column, of items
+ *  which are available to be selected.  The default is the word
+ *  <code>Available</code>. </td> </tr>
+ *
+ *  <tr>
+ *  <td>selectImage
+ * <br>selectDisabledImage
+ * <br>deselectImage
+ * <br>deselectDisabledImage
+ * <br>upImage
+ * <br>upDisabledImage
+ * <br>downImage
+ * <br>downDisabledImage
+ *  </td>
+ *  <td>{@link IAsset}</td>
+ *  <td>in</td>
+ *  <td>no</td> <td>&nbsp;</td>
+ *  <td>If any of these are specified then they override the default images provided
+ *  with the component.  This allows the look and feel to be customized relatively easily.
+ *
+ *  <p>The most common reason to replace the images is to deal with backgrounds.  The default
+ *  images are anti-aliased against a white background.  If a colored or patterned background
+ *  is used, the default images will have an ugly white fringe.  Until all browsers have full
+ *  support for PNG (which has a true alpha channel), it is necessary to customize the images
+ *  to match the background.
+ *
+ *      </td> </tr>
+ *
+ * </table>
+ *
+ * <p>A Palette requires some CSS entries to render correctly ... especially
+ * the middle column, which contains the two or four buttons for moving selections
+ * between the two columns.  The width and alignment of this column must be set
+ * using CSS.  Additionally, CSS is commonly used to give the Palette columns
+ * a fixed width, and to dress up the titles.  Here is an example of some CSS
+ * you can use to format the palette component:
+ * 
+ * <pre>
+ * TABLE.tapestry-palette TH
+ * {
+ *   font-size: 9pt;
+ *   font-weight: bold;
+ *   color: white;
+ *   background-color: #330066;
+ *   text-align: center;
+ * }
+ *
+ * TD.available-cell SELECT
+ * {
+ *   font-weight: normal;
+ *   background-color: #FFFFFF;
+ *   width: 200px;
+ * }
+ * 
+ * TD.selected-cell SELECT
+ * {
+ *   font-weight: normal;
+ *   background-color: #FFFFFF;
+ *   width: 200px;
+ * }
+ * 
+ * TABLE.tapestry-palette TD.controls
+ * {
+ *   text-align: center;
+ *   vertical-align: middle;
+ *   width: 60px;
+ * }
+ *  </pre>
+ *
+ *  @author Howard Lewis Ship
+ */
+
+public abstract class Palette extends BaseComponent implements IFormComponent
+{
+    private static final int DEFAULT_ROWS = 10;
+    private static final int MAP_SIZE = 7;
+    private static final String DEFAULT_TABLE_CLASS = "tapestry-palette";
+
+    /**
+     *  A set of symbols produced by the Palette script.  This is used to
+     *  provide proper names for some of the HTML elements (&lt;select&gt; and
+     *  &lt;button&gt; elements, etc.).
+     *
+     */
+
+    private Map _symbols;
+
+    /**
+     *  A cached copy of the script used with the component.
+     *
+     */
+
+    private IScript _script;
+
+    /** @since 3.0 **/
+    public abstract void setAvailableColumn(PaletteColumn column);
+
+    /** @since 3.0 **/
+    public abstract void setSelectedColumn(PaletteColumn column);
+
+    protected void finishLoad()
+    {
+        setSelectedTitleBlock((Block) getComponent("defaultSelectedTitleBlock"));
+        setAvailableTitleBlock((Block) getComponent("defaultAvailableTitleBlock"));
+
+        setSelectImage(getAsset("Select"));
+        setSelectDisabledImage(getAsset("SelectDisabled"));
+        setDeselectImage(getAsset("Deselect"));
+        setDeselectDisabledImage(getAsset("DeselectDisabled"));
+        setUpImage(getAsset("Up"));
+        setUpDisabledImage(getAsset("UpDisabled"));
+        setDownImage(getAsset("Down"));
+        setDownDisabledImage(getAsset("DownDisabled"));
+
+        setTableClass(DEFAULT_TABLE_CLASS);
+        setRows(DEFAULT_ROWS);
+        setSort(SortMode.NONE);
+    }
+
+    public abstract String getName();
+    public abstract void setName(String name);
+
+    public abstract IForm getForm();
+    public abstract void setForm(IForm form);
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        IForm form = Form.get(getPage().getRequestCycle());
+
+        if (form == null)
+            throw new ApplicationRuntimeException(
+                "Palette component must be wrapped by a Form.",
+                this,
+                null,
+                null);
+
+        setForm(form);
+
+        IValidationDelegate delegate = form.getDelegate();
+
+        if (delegate != null)
+            delegate.setFormComponent(this);
+
+        setName(form.getElementId(this));
+
+        if (form.isRewinding())
+            handleSubmission(cycle);
+
+        // Don't do any additional work if rewinding
+        // (some other action or form on the page).
+
+        if (!cycle.isRewinding())
+        {
+            // Lots of work to produce JavaScript and HTML for this sucker.
+
+            _symbols = new HashMap(MAP_SIZE);
+
+            runScript(cycle);
+
+            // Output symbol 'formSubmitFunctionName' is the name
+            // of a JavaScript function to execute when the form
+            // is submitted.  This is also key to the operation
+            // of the PropertySelection.
+
+            form.addEventHandler(
+                FormEventType.SUBMIT,
+                (String) _symbols.get("formSubmitFunctionName"));
+
+            constructColumns();
+        }
+
+        super.renderComponent(writer, cycle);
+    }
+
+    protected void cleanupAfterRender(IRequestCycle cycle)
+    {
+        _symbols = null;
+
+        setAvailableColumn(null);
+        setSelectedColumn(null);
+
+        super.cleanupAfterRender(cycle);
+    }
+
+    /**
+     *  Executes the associated script, which generates all the JavaScript to
+     *  support this Palette.
+     *
+     */
+    private void runScript(IRequestCycle cycle)
+    {
+        // Get the script, if not already gotten.  Scripts are re-entrant, so it is
+        // safe to share this between instances of Palette.
+
+        if (_script == null)
+        {
+            IEngine engine = getPage().getEngine();
+            IScriptSource source = engine.getScriptSource();
+
+            IResourceLocation scriptLocation =
+                getSpecification().getSpecificationLocation().getRelativeLocation("Palette.script");
+
+            _script = source.getScript(scriptLocation);
+        }
+
+        Body body = Body.get(cycle);
+        if (body == null)
+            throw new ApplicationRuntimeException(
+                "Palette component must be wrapped by a Body.",
+                this,
+                null,
+                null);
+
+        setImage(body, cycle, "selectImage", getSelectImage());
+        setImage(body, cycle, "selectDisabledImage", getSelectDisabledImage());
+        setImage(body, cycle, "deselectImage", getDeselectImage());
+        setImage(body, cycle, "deselectDisabledImage", getDeselectDisabledImage());
+
+        if (isSortUser())
+        {
+            setImage(body, cycle, "upImage", getUpImage());
+            setImage(body, cycle, "upDisabledImage", getUpDisabledImage());
+            setImage(body, cycle, "downImage", getDownImage());
+            setImage(body, cycle, "downDisabledImage", getDownDisabledImage());
+        }
+
+        _symbols.put("palette", this);
+
+        _script.execute(cycle, body, _symbols);
+    }
+
+    /**
+     *  Extracts its asset URL, sets it up for
+     *  preloading, and assigns the preload reference as a script symbol.
+     *
+     */
+    private void setImage(Body body, IRequestCycle cycle, String symbolName, IAsset asset)
+    {
+        String URL = asset.buildURL(cycle);
+        String reference = body.getPreloadedImageReference(URL);
+
+        _symbols.put(symbolName, reference);
+    }
+
+    public Map getSymbols()
+    {
+        return _symbols;
+    }
+
+    /**
+     *  Constructs a pair of {@link PaletteColumn}s: the available and selected options.
+     *
+     */
+    private void constructColumns()
+    {
+        // Build a Set around the list of selected items.
+
+        List selected = getSelected();
+
+        if (selected == null)
+            selected = Collections.EMPTY_LIST;
+
+        SortMode sortMode = getSort();
+
+        boolean sortUser = sortMode == SortMode.USER;
+
+        List selectedOptions = null;
+
+        if (sortUser)
+        {
+            int count = selected.size();
+            selectedOptions = new ArrayList(count);
+
+            for (int i = 0; i < count; i++)
+                selectedOptions.add(null);
+        }
+
+        PaletteColumn availableColumn =
+            new PaletteColumn((String) _symbols.get("availableName"), getRows());
+        PaletteColumn selectedColumn = new PaletteColumn(getName(), getRows());
+
+        // Each value specified in the model will go into either the selected or available
+        // lists.
+
+        IPropertySelectionModel model = getModel();
+
+        int count = model.getOptionCount();
+
+        for (int i = 0; i < count; i++)
+        {
+            Object optionValue = model.getOption(i);
+
+            PaletteOption o = new PaletteOption(model.getValue(i), model.getLabel(i));
+
+            int index = selected.indexOf(optionValue);
+            boolean isSelected = index >= 0;
+
+            if (sortUser && isSelected)
+            {
+                selectedOptions.set(index, o);
+                continue;
+            }
+
+            PaletteColumn c = isSelected ? selectedColumn : availableColumn;
+
+            c.addOption(o);
+        }
+
+        if (sortUser)
+        {
+            Iterator i = selectedOptions.iterator();
+            while (i.hasNext())
+            {
+                PaletteOption o = (PaletteOption) i.next();
+                selectedColumn.addOption(o);
+            }
+        }
+
+        if (sortMode == SortMode.VALUE)
+        {
+            availableColumn.sortByValue();
+            selectedColumn.sortByValue();
+        }
+        else
+            if (sortMode == SortMode.LABEL)
+            {
+                availableColumn.sortByLabel();
+                selectedColumn.sortByLabel();
+            }
+
+        setAvailableColumn(availableColumn);
+        setSelectedColumn(selectedColumn);
+    }
+
+    private void handleSubmission(IRequestCycle cycle)
+    {
+        RequestContext context = cycle.getRequestContext();
+        String[] values = context.getParameters(getName());
+
+        int count = Tapestry.size(values);
+
+        // Build a new ArrayList and fill it with the selected 
+        // objects, if any. 
+
+        List selected = new ArrayList(count);
+        IPropertySelectionModel model = getModel();
+
+        for (int i = 0; i < count; i++)
+        {
+            String value = values[i];
+            Object option = model.translateValue(value);
+
+            selected.add(option);
+        }
+
+        setSelected(selected);
+    }
+
+    public boolean isSortUser()
+    {
+        return getSort() == SortMode.USER;
+    }
+
+    /**
+     *  Returns null, but may make sense to implement a displayName parameter.
+     * 
+     */
+    public String getDisplayName()
+    {
+        return null;
+    }
+
+    public abstract Block getAvailableTitleBlock();
+
+    public abstract void setAvailableTitleBlock(Block availableTitleBlock);
+
+    public abstract IAsset getDeselectDisabledImage();
+
+    public abstract void setDeselectDisabledImage(IAsset deselectDisabledImage);
+
+    public abstract IAsset getDeselectImage();
+
+    public abstract void setDeselectImage(IAsset deselectImage);
+
+    public abstract IAsset getDownDisabledImage();
+
+    public abstract void setDownDisabledImage(IAsset downDisabledImage);
+
+    public abstract IAsset getDownImage();
+
+    public abstract void setDownImage(IAsset downImage);
+
+    public abstract IPropertySelectionModel getModel();
+
+    public abstract int getRows();
+
+    public abstract void setRows(int rows);
+
+    public abstract IAsset getSelectDisabledImage();
+
+    public abstract void setSelectDisabledImage(IAsset selectDisabledImage);
+
+    public abstract Block getSelectedTitleBlock();
+
+    public abstract void setSelectedTitleBlock(Block selectedTitleBlock);
+
+    public abstract IAsset getSelectImage();
+
+    public abstract void setSelectImage(IAsset selectImage);
+
+    public abstract SortMode getSort();
+
+    public abstract void setSort(SortMode sort);
+
+    public abstract void setTableClass(String tableClass);
+
+    public abstract IAsset getUpDisabledImage();
+
+    public abstract void setUpDisabledImage(IAsset upDisabledImage);
+
+    public abstract IAsset getUpImage();
+
+    public abstract void setUpImage(IAsset upImage);
+
+    /**
+     *  Returns false.  Palette components are never disabled.
+     * 
+     *  @since 2.2
+     * 
+     */
+
+    public boolean isDisabled()
+    {
+        return false;
+    }
+
+    /** @since 2.2 **/
+
+    public abstract List getSelected();
+
+    /**  @since 2.2 **/
+
+    public abstract void setSelected(List selected);
+
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/palette/Palette.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/Palette.jwc
new file mode 100644
index 0000000..8b130c9
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/Palette.jwc
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!--  $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.contrib.palette.Palette" 
+	allow-body="no" allow-informal-parameters="no">
+	
+  <description>
+  A complex component used to manage multiple selection of items from a list.
+  </description>
+  
+  <parameter name="selectedTitleBlock" 
+  	type="org.apache.tapestry.components.Block" 
+  	required="no"
+  	direction="in"/>
+  
+  <parameter name="availableTitleBlock" 
+   	type="org.apache.tapestry.components.Block" 
+   	required="no"
+   	direction="in"/>
+   	
+  <parameter name="model" 
+  	type="org.apache.tapestry.form.IPropertySelectionModel" 
+  	required="yes"
+  	direction="in"/>
+  
+  <parameter name="selected" type="java.util.List" 
+  	required="yes" direction="form"/>
+  
+  <parameter name="sort" 
+  	type="org.apache.tapestry.contrib.palette.SortMode" 
+  	required="no"
+  	direction="in"/>
+  
+  <parameter name="rows" 
+   	type="int" 
+   	required="no"
+   	direction="in"/>
+  
+  <parameter name="tableClass" 
+  	type="java.lang.String" 
+  	required="no"
+  	direction="in"/>
+  
+  
+  <parameter name="selectImage" type="org.apache.tapestry.IAsset" direction="in"/>
+  <parameter name="selectDisabledImage" type="org.apache.tapestry.IAsset" direction="in"/>
+  <parameter name="deselectImage" type="org.apache.tapestry.IAsset" direction="in"/>
+  <parameter name="deselectDisabledImage" type="org.apache.tapestry.IAsset" direction="in"/>
+  <parameter name="upImage" type="org.apache.tapestry.IAsset" direction="in"/>
+  <parameter name="upDisabledImage" type="org.apache.tapestry.IAsset" direction="in"/>
+  <parameter name="downImage" type="org.apache.tapestry.IAsset" direction="in"/>
+  <parameter name="downDisabledImage" type="org.apache.tapestry.IAsset" direction="in"/>
+    
+  <property-specification name="name" type="java.lang.String"/>
+  <property-specification name="form" type="org.apache.tapestry.IForm"/>
+  
+  <property-specification name="selectedColumn" type="org.apache.tapestry.contrib.palette.PaletteColumn"/>
+  <property-specification name="availableColumn" type="org.apache.tapestry.contrib.palette.PaletteColumn"/>
+  
+  
+  <private-asset name="Select" resource-path="select_right.gif"/>
+  <private-asset name="SelectDisabled" resource-path="select_right_off.gif"/>
+  <private-asset name="Deselect" resource-path="deselect_left.gif"/>
+  <private-asset name="DeselectDisabled" resource-path="deselect_left_off.gif"/>
+  <private-asset name="Up" resource-path="move_up.gif"/>
+  <private-asset name="UpDisabled" resource-path="move_up_off.gif"/>
+  <private-asset name="Down" resource-path="move_down.gif"/>
+  <private-asset name="DownDisabled" resource-path="move_down_off.gif"/>
+  
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/palette/Palette.properties b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/Palette.properties
new file mode 100644
index 0000000..6056da7
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/Palette.properties
@@ -0,0 +1,8 @@
+# $Id$
+title.available=Available
+title.selected=Selected
+
+tooltip.select=Select
+tooltip.deselect=Deselect
+tooltip.moveup=Move Up
+tooltip.movedown=Move Down
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/palette/Palette.script b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/Palette.script
new file mode 100644
index 0000000..848346c
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/Palette.script
@@ -0,0 +1,349 @@
+<?xml version="1.0"?>
+<!-- $Id$ -->
+<!DOCTYPE script PUBLIC
+	"-//Apache Software Foundation//Tapestry Script Specification 3.0//EN"
+	"http://jakarta.apache.org/tapestry/dtd/Script_3_0.dtd">
+<script>
+<!-- 
+
+input symbols:
+  palette - the Palette instance
+  selectImage - reference to the select image
+  selectDisabledImage - referece to the disabled select image  
+  deselectImage - reference to the deselect image
+  deselectDisabledImage - reference to the disbled deselect image
+  upImage - reference to the move up image
+  upDisabledImage - reference to the disabled move up image
+  downImage - reference to the move down image
+  downDisabledImage - reference to the disabled move down image
+
+Note: "reference" means the result of Body.getPreloadedImageReference().  The
+up and down images are only needed if user sorting is enabled.
+
+output symbols:
+  formSubmitFunctionName - name of a function to be executed when the form submits
+  availableName - the name of the available element
+  selectImageName - the name to use for the select image (inside the select link)
+  selectOnClickScript - the script to assign to the select link's onclick attribute
+  deselectOnClickScript - the script to assign to the deselect link's onclick attribute
+  deselectImageName - the name to use for the deselect image (inside the deselect link)
+  upImageName - the name of the up image (inside the up link)
+  downImageName the name of the move down image (inside the down link)
+  upOnClickScript - the script to assign to the up link's onclick attribute
+  downOnClickScript - the script to assign to the down link's onclick attribute
+-->
+
+<include-script resource-path="/org/apache/tapestry/html/PracticalBrowserSniffer.js"/>
+<include-script resource-path="/org/apache/tapestry/contrib/palette/PaletteFunctions.js"/>
+
+<input-symbol key="palette" class="org.apache.tapestry.contrib.palette.Palette" required="yes"/>
+<input-symbol key="selectImage" class="java.lang.String" required="yes"/>
+<input-symbol key="selectDisabledImage" class="java.lang.String" required="yes"/>
+<input-symbol key="deselectImage" class="java.lang.String" required="yes"/>
+<input-symbol key="deselectDisabledImage" class="java.lang.String" required="yes"/>
+<input-symbol key="upImage" class="java.lang.String"/>
+<input-symbol key="upDisabledImage" class="java.lang.String"/>
+<input-symbol key="downImage" class="java.lang.String"/>
+<input-symbol key="downDisabledImage" class="java.lang.String"/>
+
+<set key="formName" expression="palette.form.name"/>
+<set key="name" expression="palette.name"/>
+<set key="sortLabel" expression="palette.sort == @org.apache.tapestry.contrib.palette.SortMode@LABEL"/>
+<set key="sortValue" expression="palette.sort == @org.apache.tapestry.contrib.palette.SortMode@VALUE"/>
+<set key="sortUser" expression="palette.sort == @org.apache.tapestry.contrib.palette.SortMode@USER"/>
+
+
+<!-- baseName - base name from which other names are generated -->
+
+<let key="baseName" unique="yes">
+  ${name}
+</let>
+
+<let key="buttons">
+  ${baseName}$buttons
+</let>
+
+<let key="selectDisabled">
+  ${buttons}.selectDisabled
+</let>
+
+<let key="deselectDisabled">
+  ${buttons}.deselectDisabled
+</let>
+
+<let key="upDisabled">
+  ${buttons}.upDisabled
+</let>
+
+<let key="downDisabled">
+  ${buttons}.downDisabled
+</let>
+
+<let key="availableName">
+  ${name}$avail
+</let>
+
+<let key="updateFunctionName">
+  update_${baseName}
+</let>
+
+<let key="selectFunctionName">
+  select_${baseName}
+</let>
+
+<let key="selectOnClickScript">
+  javascript:${selectFunctionName}();
+</let>
+
+<let key="deselectFunctionName">
+  deselect_${baseName}
+</let>
+
+<let key="deselectOnClickScript">
+  javascript:${deselectFunctionName}();
+</let>
+
+<let key="formSubmitFunctionName">
+  onsubmit_${baseName}
+</let>
+
+<let key="selectImageName">
+  ${baseName}$selectImage
+</let>
+
+<let key="selectImagePath">
+  document.${selectImageName}
+</let>
+
+<let key="deselectImageName">
+  ${baseName}$deselectImage
+</let>
+
+<let key="deselectImagePath">
+  document.${deselectImageName}
+</let>
+
+<let key="formPath">
+  document.${formName}
+</let>
+
+<let key="selectedPath">
+  ${formPath}.${name}
+</let>
+
+<let key="selectedChangeFunctionName">
+  onChange_${baseName}_selected
+</let>
+
+<let key="availablePath">
+  ${formPath}.${availableName}
+</let>
+
+<let key="availableChangeFunctionName">
+  onChange_${baseName}_available
+</let>
+
+
+<let key="upImageName">
+  ${baseName}$upimage
+</let>
+
+<let key="upImagePath">
+  document.${upImageName}
+</let>
+
+<let key="downImageName">
+  ${baseName}$downimage
+</let>
+
+<let key="downImagePath">
+  document.${downImageName}
+</let>
+
+<let key="moveUpFunctionName">
+  moveup_${baseName}
+</let>
+
+<let key="upOnClickScript">
+  javascript:${moveUpFunctionName}();
+</let>
+
+<let key="moveDownFunctionName">
+  movedown_${baseName}
+</let>
+
+<let key="downOnClickScript">
+  javascript:${moveDownFunctionName}();
+</let>
+
+
+<body>
+
+<!-- A variable that is used to track which of the buttons are enabled
+     or disabled. All of the buttons are disabled until the page finishes
+     loading, at which point the update function will determine which
+     can be used. -->
+     
+var ${buttons} = new Object();
+${selectDisabled} = true;
+${deselectDisabled} = true;
+<if expression="sortUser">
+${upDisabled} = true;
+${downDisabled} = true;
+</if>
+
+function ${updateFunctionName}()
+{
+  var disabled = ${availablePath}.selectedIndex &lt; 0;
+  
+  ${selectDisabled} = disabled;
+ 
+  if (document.images)
+    ${selectImagePath}.src =
+      disabled ? ${selectDisabledImage}
+               : ${selectImage};
+
+  var selected = ${selectedPath};
+  var index = selected.selectedIndex;
+
+  disabled = index &lt; 0;
+  ${deselectDisabled} = disabled;
+ 
+  if (document.images)
+    ${deselectImagePath}.src =
+      disabled ? ${deselectDisabledImage}
+               : ${deselectImage};   
+<if expression="sortUser">
+  var upImage = ${upImagePath};
+  var downImage = ${downImagePath};
+  
+  ${upDisabled} = true;
+  ${downDisabled} = true;
+  
+  if (document.images)
+  {
+    upImage.src = ${upDisabledImage};
+    downImage.src = ${downDisabledImage};
+  }
+  
+  <!-- If there's no selection in the "selected" column, then leave
+       both buttons disabled. -->
+       
+  if (disabled)
+    return;
+ 
+  <!-- Search for a second selected item -->
+  
+  for (var i = index + 1; i &lt; selected.options.length; i++)
+  {
+    <!-- Found a second selected option, so leave buttons disabled. -->
+    if (selected.options[i].selected)
+    return;
+  }
+  
+  ${upDisabled} = (index == 0);
+  ${downDisabled} = (index == selected.options.length - 1);  
+  
+  if (document.images)
+  {
+    if (!${upDisabled})
+      upImage.src = ${upImage};
+
+    if (!${downDisabled})
+      downImage.src = ${downImage};
+  }
+</if>  
+}
+
+function ${selectFunctionName}()
+{
+ if (${selectDisabled})
+    return;
+    
+  var source = ${availablePath};
+  var target = ${selectedPath};
+     
+  palette_transfer_selections(source, target);
+<if expression="sortLabel">
+  palette_sort_by_label(target);
+</if>
+<if expression="sortValue">
+  palette_sort_by_value(target);
+</if>
+  ${updateFunctionName}();
+}
+
+function ${deselectFunctionName}()
+{
+  if (${deselectDisabled})
+    return;
+  
+  var source = ${selectedPath};
+  var target = ${availablePath};
+  
+  palette_transfer_selections(source, target);
+<if expression="sortLabel">
+  palette_sort_by_label(target);
+</if>
+<if expression="sortValue">
+  palette_sort_by_value(target);
+</if>
+  ${updateFunctionName}();  
+}
+
+function ${formSubmitFunctionName}()
+{
+  palette_clear_selections(${availablePath});
+  palette_select_all(${selectedPath});
+  
+  return true;
+}
+<if expression="sortUser">
+function ${moveUpFunctionName}()
+{
+  if (${upDisabled})
+    return;
+    
+  var element = ${selectedPath};
+  var options = element.options;
+    
+  palette_swap_options(options, element.selectedIndex, element.selectedIndex - 1);
+  
+  ${updateFunctionName}();
+}
+
+function ${moveDownFunctionName}()
+{
+  if (${downDisabled})
+    return;
+    
+  var element = ${selectedPath};
+  var options = element.options;
+  
+  palette_swap_options(options, element.selectedIndex, element.selectedIndex + 1);
+  
+  ${updateFunctionName}();
+}
+</if>
+function ${selectedChangeFunctionName}()
+{
+  palette_clear_selections(${availablePath});
+  ${updateFunctionName}();
+}
+
+function ${availableChangeFunctionName}()
+{
+  palette_clear_selections(${selectedPath});
+  ${updateFunctionName}();
+}
+</body>
+
+<initialization>
+
+${selectedPath}.onchange = ${selectedChangeFunctionName};
+${selectedPath}.ondblclick = ${deselectFunctionName};
+${availablePath}.onchange = ${availableChangeFunctionName};
+${availablePath}.ondblclick = ${selectFunctionName};
+
+</initialization>
+</script>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/palette/PaletteColumn.java b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/PaletteColumn.java
new file mode 100644
index 0000000..89aae9a
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/PaletteColumn.java
@@ -0,0 +1,118 @@
+//  Copyright 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.tapestry.contrib.palette;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ * One of the two columns in a Palette component: the left column lists
+ * available options, the right column lists the selected columns.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ */
+public class PaletteColumn implements IRender
+{
+    private String _name;
+    private int _rows;
+    private List _options = new ArrayList();
+
+    private static class ValueComparator implements Comparator
+    {
+        public int compare(Object o1, Object o2)
+        {
+            PaletteOption option1 = (PaletteOption) o1;
+            PaletteOption option2 = (PaletteOption) o2;
+
+            return option1.getValue().compareTo(option2.getValue());
+        }
+    }
+
+    private static class LabelComparator implements Comparator
+    {
+        public int compare(Object o1, Object o2)
+        {
+            PaletteOption option1 = (PaletteOption) o1;
+            PaletteOption option2 = (PaletteOption) o2;
+
+            return option1.getLabel().compareTo(option2.getLabel());
+        }
+    }
+
+	/**
+	 * @param name the name of the column (the name attribute of the &lt;select&gt;)
+	 * @param rows the number of visible rows (the size attribute of the &lt;select&gt;)
+	 */
+	public PaletteColumn(String name, int rows)
+	{
+		_name = name;
+		_rows = rows;
+	}
+
+    public void addOption(PaletteOption option)
+    {
+        _options.add(option);
+    }
+
+    /**
+     * Sorts the options by value (the hidden value for the option
+     * that represents the object value). This should be invoked
+     * before rendering this PaletteColumn.
+     */
+    public void sortByValue()
+    {
+        Collections.sort(_options, new ValueComparator());
+    }
+
+    /**
+     * Sorts the options by the label visible to the user. This should be invoked
+     * before rendering this PaletteColumn.
+     */
+    public void sortByLabel()
+    {
+        Collections.sort(_options, new LabelComparator());
+    }
+
+    /**
+     * Renders the &lt;select&gt; and &lt;option&gt; tags for
+     * this column.
+     */
+    public void render(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        writer.begin("select");
+        writer.attribute("multiple", "multiple");
+        writer.attribute("name", _name);
+        writer.attribute("size", _rows);
+        writer.println();
+
+        int count = _options.size();
+        for (int i = 0; i < count; i++)
+        {
+            PaletteOption o = (PaletteOption) _options.get(i);
+
+            o.render(writer, cycle);
+        }
+
+        writer.end();
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/palette/PaletteFunctions.js b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/PaletteFunctions.js
new file mode 100644
index 0000000..4973536
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/PaletteFunctions.js
@@ -0,0 +1,178 @@
+// $Id: PaletteFunctions.js,v 1.3 2002/05/03 20:03:06 hship Exp $

+// Requires: /org/apache/tapestry/html/PracticalBrowserSniffer.js

+

+function palette_clear_selections(element)

+{

+  var options = element.options;

+  

+  for (var i = 0; i < options.length; i++)

+    options[i].selected = false;

+}

+

+function palette_select_all(element)

+{

+  var options = element.options;

+

+  for (var i = 0; i < options.length; i++)

+    options[i].selected = true;

+}

+

+function palette_sort(element, sorter)

+{

+  var options = element.options;

+  var list = new Array();

+  var index = 0;

+  var isNavigator = (navigator.family == "nn4" || navigator.family == "gecko");

+  

+  while (options.length > 0)

+  {

+    var option = options[0];

+        

+    if (isNavigator)

+    {

+      // Can't transfer option in nn4, nn6

+      

+     if (navigator.family == 'gecko')

+      	var copy = document.createElement("OPTION");

+     else

+        var copy = new Option(option.text, option.value);

+

+      	copy.text = option.text;

+      	copy.value = option.value;

+      	copy.selected = options.selected;

+      	

+      list[index++] = copy;

+    }

+    else

+      list[index++] = option;

+

+    

+    options[0] = null;

+  }

+  

+  list.sort(sorter);

+  

+  for (var i = 0; i < list.length; i++)

+  {

+    options[i] = list[i]; 

+  }

+

+

+}

+

+function palette_label_sorter(a, b)

+{

+  var a_text = a.text;

+  var b_text = b.text;

+  

+  if (a_text == b_text)

+    return 0;

+    

+  if (a_text < b.text)

+    return -1;

+    

+  return 1;

+}

+

+function palette_sort_by_label(element)

+{

+  palette_sort(element, palette_label_sorter);

+}

+

+function palette_value_sorter(a, b)

+{

+  var a_value = a.value;

+  var b_value = b.value;

+  

+  if (a_value == b_value)

+    return 0;

+    

+  if (a_value < b_value)

+    return -1;

+    

+  return 1;

+}

+

+function palette_sort_by_value(element)

+{

+  palette_sort(element, palette_value_sorter);

+}

+  

+function palette_transfer_selections(source, target)

+{

+  var sourceOptions = source.options;

+  var targetOptions = target.options;

+  

+  var targetIndex = target.selectedIndex;

+  var offset = 0;

+  

+  palette_clear_selections(target);

+  

+  for (var i = 0; i < sourceOptions.length; i++)

+  {

+    var option = sourceOptions[i];

+    

+    if (option.selected)

+    {

+

+       if (navigator.family == 'nn4' || navigator.family == 'gecko')

+       {

+           // Can't share options between selects in NN4

+           

+           var newOption = new Option(option.text, option.value, false, true);

+ 

+           sourceOptions[i] = null;

+      

+          // Always added to end in NN4

+                     

+          targetOptions[targetOptions.length] = newOption;

+       }

+       else

+       {  

+         sourceOptions.remove(i);

+         

+         if (targetIndex < 0)

+           targetOptions.add(option);

+         else

+           targetOptions.add(option, targetIndex + offset++);

+      }

+    

+      i--;

+    }

+  }

+

+}

+

+function palette_swap_options(options, selectedIndex, targetIndex)

+{

+  var option = options[selectedIndex];

+

+  // It's very hard to reorder options in NN4

+  

+  if (navigator.family == 'nn4' || navigator.family == 'gecko')

+  {

+    var swap = options[targetIndex];

+    

+    var hold = swap.text;

+    swap.text = option.text;

+    option.text = hold;

+    

+    hold = swap.value;

+    swap.value = option.value;

+    option.value = hold;

+    

+    hold = swap.selected;

+    swap.selected = option.selected;

+    option.selected = hold;

+    

+    // defaultSelected isn't relevant to the Palette

+    

+    return;

+  }

+  

+  // Sensible browsers ...

+  

+  options.remove(selectedIndex);

+  options.add(option, targetIndex);

+}

+

diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/palette/PaletteOption.java b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/PaletteOption.java
new file mode 100644
index 0000000..4c2d478
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/PaletteOption.java
@@ -0,0 +1,58 @@
+//  Copyright 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.tapestry.contrib.palette;
+
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ * Used to hold options editable by a Palette component, so that they may
+ * be sorted into an appropriate order.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ */
+public class PaletteOption implements IRender
+{
+    private String _value;
+    private String _label;
+
+    public PaletteOption(String value, String label)
+    {
+        _value = value;
+        _label = label;
+    }
+
+    public void render(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        writer.begin("option");
+        writer.attribute("value", _value);
+        writer.print(_label);
+        writer.end(); // <option>
+        writer.println();
+    }
+
+    public String getLabel()
+    {
+        return _label;
+    }
+
+    public String getValue()
+    {
+        return _value;
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/palette/SortMode.java b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/SortMode.java
new file mode 100644
index 0000000..ab8c082
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/SortMode.java
@@ -0,0 +1,64 @@
+//  Copyright 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.tapestry.contrib.palette;
+
+import org.apache.commons.lang.enum.Enum;
+
+/**
+ *  Defines different sorting strategies for the {@link Palette} component.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class SortMode extends Enum
+{
+    /**
+     *  Sorting is not relevant and no sort controls should be visible.
+     *
+     **/
+
+    public static final SortMode NONE = new SortMode("NONE");
+
+    /**
+     * Options should be sorted by their label.
+     *
+     **/
+
+    public static final SortMode LABEL = new SortMode("LABEL");
+
+    /**
+     *  Options should be sorted by thier value.
+     *
+     **/
+
+    public static final SortMode VALUE = new SortMode("VALUE");
+
+    /**
+     *  The user controls sort order; additional controls are added
+     *  to allow the user to control the order of options in the
+     *  selected list.
+     *
+     **/
+
+    public static final SortMode USER = new SortMode("USER");
+
+    private SortMode(String name)
+    {
+        super(name);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/palette/deselect_left.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/deselect_left.gif
new file mode 100644
index 0000000..2940e01
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/deselect_left.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/palette/deselect_left_off.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/deselect_left_off.gif
new file mode 100644
index 0000000..84dc945
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/deselect_left_off.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/palette/move_down.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/move_down.gif
new file mode 100644
index 0000000..8fb0088
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/move_down.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/palette/move_down_off.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/move_down_off.gif
new file mode 100644
index 0000000..174baeb
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/move_down_off.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/palette/move_up.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/move_up.gif
new file mode 100644
index 0000000..711c86a
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/move_up.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/palette/move_up_off.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/move_up_off.gif
new file mode 100644
index 0000000..e23a43f
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/move_up_off.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/palette/select_right.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/select_right.gif
new file mode 100644
index 0000000..74e90c3
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/select_right.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/palette/select_right_off.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/select_right_off.gif
new file mode 100644
index 0000000..452ce50
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/palette/select_right_off.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/popup/PopupLink.html b/tapestry-contrib/src/org/apache/tapestry/contrib/popup/PopupLink.html
new file mode 100644
index 0000000..63d5ed5
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/popup/PopupLink.html
@@ -0,0 +1,3 @@
+<!-- $Id$ -->
+<span jwcid="popoutScript"/>
+<span jwcid="link"><span jwcid="wrapped"/></span>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/popup/PopupLink.java b/tapestry-contrib/src/org/apache/tapestry/contrib/popup/PopupLink.java
new file mode 100644
index 0000000..6810077
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/popup/PopupLink.java
@@ -0,0 +1,128 @@
+//  Copyright 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.tapestry.contrib.popup;
+
+import java.io.UnsupportedEncodingException;
+
+import org.apache.commons.codec.net.URLCodec;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.Tapestry;
+
+/**
+ * This component provides a popup link to launch a new window using a given
+ * href, windowName and windowFeatures for the javascript function:
+ * <tt>window.open(URL, windowName, windowFeatures)</tt>.
+ *
+ *  [<a href="../../../../../../ComponentReference/contrib.PopupLink.html">Component Reference</a>]
+ * 
+ * @version $Id$ 
+ * @author Joe Panico
+ */
+public class PopupLink extends BaseComponent
+{
+	/** The default popup window name 'popuplink_window'. */
+	public static final String DEFAULT_WINDOW_NAME = "popuplink_window";
+    private static final URLCodec _urlCodec = new URLCodec();
+
+	//	Instance variables
+	private IBinding _hrefBinding;
+	private IBinding _windowNameBinding;
+	private IBinding _featuresBinding;
+
+	public IBinding getHrefBinding()
+	{
+		return _hrefBinding;
+	}
+
+	public void setHrefBinding(IBinding hrefBinding)
+	{
+		_hrefBinding = hrefBinding;
+	}
+
+	public IBinding getWindowNameBinding()
+	{
+		return _windowNameBinding;
+	}
+
+	public void setWindowNameBinding(IBinding windowNameBinding)
+	{
+		_windowNameBinding = windowNameBinding;
+	}
+
+	public IBinding getFeaturesBinding()
+	{
+		return _featuresBinding;
+	}
+
+	public void setFeaturesBinding(IBinding featuresBinding)
+	{
+		_featuresBinding = featuresBinding;
+	}
+
+	public String getHref()
+	{
+		IBinding aHrefBinding = getHrefBinding();
+
+		if (aHrefBinding != null)
+		{
+            String encoding = getPage().getEngine().getOutputEncoding();
+            try
+            {
+                return _urlCodec.encode(aHrefBinding.getString(), encoding);
+            }
+            catch (UnsupportedEncodingException e)
+            {
+                throw new ApplicationRuntimeException(
+                    Tapestry.format("illegal-encoding", encoding),
+                    e);
+            }
+		}
+
+		return null;
+	}
+
+	public String getWindowName()
+	{
+		IBinding aWindowNameBinding = getWindowNameBinding();
+		if (aWindowNameBinding != null)
+		{
+			return aWindowNameBinding.getString();
+		}
+		else
+		{
+			return DEFAULT_WINDOW_NAME;
+		}
+	}
+
+	public String getFeatures()
+	{
+		IBinding aFeaturesBinding = getFeaturesBinding();
+		if (aFeaturesBinding != null)
+		{
+			return aFeaturesBinding.getString();
+		}
+		else
+		{
+			return "";
+		}
+	}
+    
+    public String getPopupFunctionName()
+    {
+        return getIdPath().replace('.', '_') + "_popup";
+    }
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/popup/PopupLink.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/popup/PopupLink.jwc
new file mode 100644
index 0000000..21e9fc4
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/popup/PopupLink.jwc
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.contrib.popup.PopupLink" allow-body="yes" allow-informal-parameters="yes">
+
+	<parameter name="href" type="String" direction="custom" required="yes"/>
+	<parameter name="windowName" type="String" direction="custom" required="no"/>
+	<parameter name="features" type="String" direction="custom" required="no"/>
+
+	<component id="popoutScript" type="Script">
+		<binding name="script" expression='"popup.script"'/>
+		<binding name="popupFunctionName" expression="popupFunctionName"/>
+        <binding name="url" expression="href"/>
+		<binding name="windowName" expression="windowName"/>
+        <binding name="features" expression="features"/>
+	</component>
+
+  	<component id="link" type="Any" inherit-informal-parameters="yes">
+        <binding name="element" expression='"a"'/>
+        <binding name="href" expression='"javascript:" + popupFunctionName + "();"'/>
+  	</component>
+
+	<component id="wrapped" type="RenderBody"/>
+
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/popup/popup.script b/tapestry-contrib/src/org/apache/tapestry/contrib/popup/popup.script
new file mode 100644
index 0000000..7035669
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/popup/popup.script
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<!-- $Id$ -->
+<!DOCTYPE script PUBLIC 
+	"-//Howard Ship//Tapestry Script 1.1//EN"
+	"http://tapestry.sf.net/dtd/Script_1_1.dtd">
+
+<script>
+
+<body>
+function ${popupFunctionName}()
+{
+	aWindow = window.open('${url}', '${windowName}', '${features}', false);
+	aWindow.focus();
+}
+</body>
+
+</script>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/AbstractTableRowComponent.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/AbstractTableRowComponent.java
new file mode 100644
index 0000000..1501177
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/AbstractTableRowComponent.java
@@ -0,0 +1,51 @@
+//  Copyright 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.tapestry.contrib.table.components;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.table.model.ITableRowSource;
+
+/**
+ * The base implementation for a component that is wrapped by 
+ * the TableRows component. Provides a utility method for getting 
+ * a pointer to TableRows. 
+ * 
+ * @author mindbridge
+ * @version $Id$
+ *
+ */
+public class AbstractTableRowComponent extends AbstractTableViewComponent
+{
+    public ITableRowSource getTableRowSource()
+    {
+        IRequestCycle objCycle = getPage().getRequestCycle();
+
+        Object objSourceObj = objCycle.getAttribute(ITableRowSource.TABLE_ROW_SOURCE_ATTRIBUTE);
+        ITableRowSource objSource = (ITableRowSource) objSourceObj;
+
+        if (objSource == null)
+            throw new ApplicationRuntimeException(
+                "The component "
+                    + getId()
+                    + " must be contained within an ITableRowSource component, such as TableRows",
+                this,
+                null,
+                null);
+
+        return objSource;
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/AbstractTableViewComponent.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/AbstractTableViewComponent.java
new file mode 100644
index 0000000..bd6456d
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/AbstractTableViewComponent.java
@@ -0,0 +1,53 @@
+//  Copyright 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.tapestry.contrib.table.components;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.table.model.ITableModelSource;
+
+/**
+ * The base implementation for a component that is wrapped by 
+ * the TableView component. Provides a utility method for getting 
+ * a pointer to TableView. 
+ * 
+ * @author mindbridge
+ * @version $Id$
+ *
+ */
+public class AbstractTableViewComponent extends BaseComponent
+{
+    public ITableModelSource getTableModelSource()
+    {
+        IRequestCycle objCycle = getPage().getRequestCycle();
+
+        ITableModelSource objSource =
+            (ITableModelSource) objCycle.getAttribute(
+                ITableModelSource.TABLE_MODEL_SOURCE_ATTRIBUTE);
+
+        if (objSource == null)
+            throw new ApplicationRuntimeException(
+                "The component "
+                    + getId()
+                    + " must be contained within an ITableModelSource component, such as TableView",
+                this,
+                null,
+                null);
+
+        return objSource;
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/FormTable.html b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/FormTable.html
new file mode 100644
index 0000000..a4bef19
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/FormTable.html
@@ -0,0 +1,13 @@
+<!-- $Id$ -->
+
+<span jwcid="$content$">
+
+<span jwcid="tableView">
+	<span jwcid="condPages"><span jwcid="tablePages"/></span>
+	<table jwcid="tableElement">
+		<tr><span jwcid="tableColumns"/></tr>
+		<tr jwcid="tableRows"><td jwcid="tableValues"/></tr>
+	</table>
+</span>
+
+</span>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/FormTable.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/FormTable.java
new file mode 100644
index 0000000..75dc316
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/FormTable.java
@@ -0,0 +1,47 @@
+//  Copyright 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.tapestry.contrib.table.components;
+
+import org.apache.tapestry.contrib.table.model.ITableModelSource;
+
+/**
+ * A modified version of the facade component in the Table family. 
+ * FormTable allows you to present a sortable and pagable table 
+ * within a form by using only this one component.
+ * 
+ *  [<a href="../../../../../../../ComponentReference/contrib.FormTable.html">Component Reference</a>]
+ * 
+ * @author mindbridge
+ * @version $Id$
+ *
+ */
+public abstract class FormTable extends Table implements ITableModelSource
+{
+    // parameters
+    public abstract Object getColumns();
+
+    /**
+     *  If the columns are defined via a String, make sure they use 
+     *  the form-specific column headers.
+     */
+    public Object getFormColumns()
+    {
+        Object objColumns = getColumns();
+        if (objColumns instanceof String)
+            objColumns = "*" + objColumns;
+        return objColumns;
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/FormTable.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/FormTable.jwc
new file mode 100644
index 0000000..2003fb3
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/FormTable.jwc
@@ -0,0 +1,245 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!--  $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.contrib.table.components.FormTable" 
+	allow-body="yes" allow-informal-parameters="yes">
+
+    <description>
+        The main Table component that is implemented using the lower-level 
+        Table components such as TableView and TableRows.
+        The component does not render its body, which makes it a good place
+        to declare Blocks defining the column appearances.
+    </description>
+    
+    <parameter name="tableModel" 
+        type="org.apache.tapestry.contrib.table.model.ITableModel" 
+        required="no">
+        <description>
+            The model describing the data to be presented by the Table component.
+            This parameter is optional, but either the 'tableModel' or both
+            'source' and 'columns' parameters must be provided.
+        </description>
+    </parameter>
+    
+    <parameter name="source" type="java.lang.Object" required="no">
+        <description>
+            The data to be displayed by the component. This parameter is available as
+            an alternative to tableModel and must be used in combination with the
+            'columns' parameter. 
+            The parameter must be an array of values, a collection, an iterator, 
+            or an object implementing the IBasicTableModel interface.
+        </description>
+    </parameter>
+    
+    <parameter name="columns" type="java.lang.Object" required="no" direction="auto" default-value="null">
+        <description>
+            The table columns to be displayed. 
+            The parameter must be an array, a list, or an Iterator of ITableColumn objects,
+            an ITableColumnModel, or a String describing the columns (see documentation).
+        </description>
+    </parameter>
+    
+	<parameter name="pageSize" 
+		type="int" 
+		required="no">
+        <description>
+            The number of records displayed per page when source/columns are used.
+            The page size is 10 by default.
+        </description>
+    </parameter>
+  
+	<parameter name="initialSortColumn" 
+		type="java.lang.String" 
+		required="no">
+        <description>
+            The id of the column to initially sort the table by.
+            The column is set to null by default, i.e. there is no sorting.
+        </description>
+    </parameter>
+  
+	<parameter name="initialSortOrder" 
+		type="boolean" 
+		required="no">
+        <description>
+            The order of the initial sorting.
+            Set this parameter to 'false' to sort in an ascending order
+            and to 'true' to sort in a descending one.
+        </description>
+    </parameter>
+  
+    <parameter name="tableSessionStateManager" 
+        type="org.apache.tapestry.contrib.table.model.ITableSessionStateManager" 
+        required="no">
+        <description>
+            The manager that controls what part of the table model will be stored in 
+            the session.
+        </description>
+    </parameter>
+    
+    <parameter name="tableSessionStoreManager" 
+        type="org.apache.tapestry.contrib.table.model.ITableSessionStoreManager" 
+        required="no">
+        <description>
+            The manager that controls where the session data will be stored.
+        </description>
+    </parameter>
+    
+    <parameter name="columnSettingsContainer"
+        type="org.apache.tapestry.IComponent"
+        required="no"
+        default-value="container">
+        <description>
+            The component where Block and messages are pulled from when using source/columns.
+        </description>
+    </parameter>
+
+    <parameter name="convertor"
+    	type="org.apache.tapestry.contrib.table.model.IPrimaryKeyConvertor"
+    	required="no"
+    	direction="auto"
+    	default-value="null">
+        <description>
+        An interface defining how the items iterated upon by this component 
+        will be stored in the form as Hidden values. This interface allows only 
+        the primary key of the items to be stored, rather than the whole item.
+        </description>
+    </parameter>
+
+	<parameter name="pagesDisplayed" 
+		type="int" 
+		required="no">
+        <description>
+            The maximum number of pages that will be displayed in the list of table pages.
+            By default, only seven of the pages around the current one are shown.
+        </description>
+    </parameter>
+  
+	<parameter name="column" 
+		type="org.apache.tapestry.contrib.table.model.ITableColumn" 
+		required="no">
+        <description>
+            The column that is being rendered. This value is updated when both 
+            the column headers and column values are rendered.
+        </description>
+    </parameter>
+
+	<parameter name="row" 
+		type="Object" 
+		required="no"
+		direction="custom">
+        <description>
+            The row that is being rendered. This value is null when 
+            the column headers are rendered.
+        </description>
+    </parameter>
+
+	<parameter name="arrowUpAsset" 
+		type="org.apache.tapestry.IAsset" 
+		required="no"
+		direction="in">
+        <description>
+            The image to use to describe a column sorted in an ascending order.
+        </description>
+    </parameter>
+
+	<parameter name="arrowDownAsset" 
+		type="org.apache.tapestry.IAsset" 
+		required="no"
+		direction="in">
+        <description>
+            The image to use to describe a column sorted in a descending order.
+        </description>
+    </parameter>
+
+	<parameter name="pagesClass" 
+		type="java.lang.String" 
+		required="no"
+		direction="custom">
+        <description>The CSS class of the table pages</description>
+    </parameter>
+  
+	<parameter name="columnsClass" 
+		type="java.lang.String" 
+		required="no"
+		direction="custom">
+        <description>The CSS class of the table columns</description>
+    </parameter>
+  
+	<parameter name="rowsClass" 
+		type="java.lang.String" 
+		required="no"
+		direction="custom">
+        <description>The CSS class of the table rows</description>
+    </parameter>
+  
+	<parameter name="valuesClass" 
+		type="java.lang.String" 
+		required="no"
+		direction="custom">
+        <description>The CSS class of the table values</description>
+    </parameter>
+    
+
+	<component id="tableElement" type="Any" inherit-informal-parameters="yes">
+		<static-binding name="element">table</static-binding>
+	</component>
+
+	<component id="condPages" type="FormConditional">
+		<binding name="condition" expression="tableModel.pageCount > 1"/>
+	</component>
+
+	<component id="tableView" type="TableView">
+		<inherited-binding name="tableModel" parameter-name="tableModel"/>
+		<inherited-binding name="source" parameter-name="source"/>
+		<binding name="columns" expression="formColumns"/>
+		<inherited-binding name="pageSize" parameter-name="pageSize"/>
+		<inherited-binding name="initialSortColumn" parameter-name="initialSortColumn"/>
+		<inherited-binding name="initialSortOrder" parameter-name="initialSortOrder"/>
+		<inherited-binding name="tableSessionStateManager" parameter-name="tableSessionStateManager"/>
+		<inherited-binding name="tableSessionStoreManager" parameter-name="tableSessionStoreManager"/>
+		<inherited-binding name="columnSettingsContainer" parameter-name="columnSettingsContainer"/>
+		<static-binding name="element">span</static-binding>
+	</component>
+
+	<component id="tablePages" type="TableFormPages">
+		<inherited-binding name="pagesDisplayed" parameter-name="pagesDisplayed"/>
+		<inherited-binding name="class" parameter-name="pagesClass"/>
+	</component>
+
+	<component id="tableColumns" type="TableColumns">
+		<inherited-binding name="column" parameter-name="column"/>
+		<inherited-binding name="class" parameter-name="columnsClass"/>
+        <inherited-binding name="arrowUpAsset" parameter-name="arrowUpAsset"/>
+        <inherited-binding name="arrowDownAsset" parameter-name="arrowDownAsset"/>
+	</component>
+
+	<component id="tableRows" type="TableFormRows">
+		<inherited-binding name="row" parameter-name="row"/>
+		<inherited-binding name="class" parameter-name="rowsClass"/>
+    <inherited-binding name="convertor" expression="convertor"/>
+	</component>
+
+	<component id="tableValues" type="TableValues">
+		<inherited-binding name="column" parameter-name="column"/>
+		<inherited-binding name="class" parameter-name="valuesClass"/>
+	</component>
+  
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/Table.html b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/Table.html
new file mode 100644
index 0000000..a4bef19
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/Table.html
@@ -0,0 +1,13 @@
+<!-- $Id$ -->
+
+<span jwcid="$content$">
+
+<span jwcid="tableView">
+	<span jwcid="condPages"><span jwcid="tablePages"/></span>
+	<table jwcid="tableElement">
+		<tr><span jwcid="tableColumns"/></tr>
+		<tr jwcid="tableRows"><td jwcid="tableValues"/></tr>
+	</table>
+</span>
+
+</span>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/Table.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/Table.java
new file mode 100644
index 0000000..c02b913
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/Table.java
@@ -0,0 +1,111 @@
+//  Copyright 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.tapestry.contrib.table.components;
+
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+import org.apache.tapestry.contrib.table.model.ITableModel;
+import org.apache.tapestry.contrib.table.model.ITableModelSource;
+
+/**
+ * The facade component in the Table family. Table allows you to present 
+ * a sortable and pagable table simply and easily by using only this one component.
+ * Please see the Component Reference for details on how to use this component. 
+ * 
+ *  [<a href="../../../../../../../ComponentReference/contrib.Table.html">Component Reference</a>]
+ * 
+ * @author mindbridge
+ * @version $Id$
+ *
+ */
+public class Table extends BaseComponent implements ITableModelSource
+{
+    /**
+     * @see org.apache.tapestry.contrib.table.model.ITableModelSource#getTableModel()
+     */
+    public ITableModel getTableModel()
+    {
+        return getTableViewComponent().getTableModel();
+    }
+
+    /**
+     * Indicates that the table model has changed and it may need to saved.
+     * This method has to be invoked if modifications are made to the model.
+     *  
+     * @see org.apache.tapestry.contrib.table.model.ITableModelSource#fireObservedStateChange()
+     */
+    public void fireObservedStateChange()
+    {
+        getTableViewComponent().fireObservedStateChange();
+    }
+
+    /**
+     * Resets the state of the component and forces it to load a new
+     * TableModel from the tableModel binding the next time it renders.
+     */
+    public void reset()
+    {
+        getTableViewComponent().reset();
+    }
+
+    /**
+     * Returns the currently rendered table column. 
+     * You can call this method to obtain the current column.
+     *  
+     * @return ITableColumn the current table column
+     */
+    public ITableColumn getTableColumn()
+    {
+        Object objCurrentRow = getTableRow();
+
+        // if the current row is null, then we are most likely rendering TableColumns
+        if (objCurrentRow == null)
+            return getTableColumnsComponent().getTableColumn();
+        else
+            return getTableValuesComponent().getTableColumn();
+    }
+
+    /**
+     * Returns the currently rendered table row or null 
+     * if the rows are not rendered at the moment.
+     * You can call this method to obtain the current row.
+     *  
+     * @return Object the current table row 
+     */
+    public Object getTableRow()
+    {
+        return getTableRowsComponent().getTableRow();
+    }
+
+    protected TableView getTableViewComponent()
+    {
+        return (TableView) getComponent("tableView");
+    }
+
+    protected TableColumns getTableColumnsComponent()
+    {
+        return (TableColumns) getComponent("tableColumns");
+    }
+
+    protected TableRows getTableRowsComponent()
+    {
+        return (TableRows) getComponent("tableRows");
+    }
+
+    protected TableValues getTableValuesComponent()
+    {
+        return (TableValues) getComponent("tableValues");
+    }
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/Table.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/Table.jwc
new file mode 100644
index 0000000..06b0360
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/Table.jwc
@@ -0,0 +1,233 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!--  $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.contrib.table.components.Table" 
+	allow-body="yes" allow-informal-parameters="yes">
+
+    <description>
+        The main Table component that is implemented using the lower-level 
+        Table components such as TableView and TableRows.
+        The component does not render its body, which makes it a good place
+        to declare Blocks defining the column appearances.
+    </description>
+    
+    <parameter name="tableModel" 
+        type="org.apache.tapestry.contrib.table.model.ITableModel" 
+        required="no">
+        <description>
+            The model describing the data to be presented by the Table component.
+            This parameter is optional, but either the 'tableModel' or both
+            'source' and 'columns' parameters must be provided.
+        </description>
+    </parameter>
+    
+    <parameter name="source" type="java.lang.Object" required="no">
+        <description>
+            The data to be displayed by the component. This parameter is available as
+            an alternative to tableModel and must be used in combination with the
+            'columns' parameter. 
+            The parameter must be an array of values, a collection, an iterator, 
+            or an object implementing the IBasicTableModel interface.
+        </description>
+    </parameter>
+    
+    <parameter name="columns" type="java.lang.Object" required="no">
+        <description>
+            The table columns to be displayed. 
+            The parameter must be an array, a list, or an Iterator of ITableColumn objects,
+            an ITableColumnModel, or a String describing the columns (see documentation).
+        </description>
+    </parameter>
+    
+	<parameter name="pageSize" 
+		type="int" 
+		required="no">
+        <description>
+            The number of records displayed per page when source/columns are used.
+            The page size is 10 by default.
+        </description>
+    </parameter>
+  
+	<parameter name="initialSortColumn" 
+		type="java.lang.String" 
+		required="no">
+        <description>
+            The id of the column to initially sort the table by.
+            The column is set to null by default, i.e. there is no sorting.
+        </description>
+    </parameter>
+  
+	<parameter name="initialSortOrder" 
+		type="boolean" 
+		required="no">
+        <description>
+            The order of the initial sorting.
+            Set this parameter to 'false' to sort in an ascending order
+            and to 'true' to sort in a descending one.
+        </description>
+    </parameter>
+  
+    <parameter name="tableSessionStateManager" 
+        type="org.apache.tapestry.contrib.table.model.ITableSessionStateManager" 
+        required="no">
+        <description>
+            The manager that controls what part of the table model will be stored in 
+            the session.
+        </description>
+    </parameter>
+    
+    <parameter name="tableSessionStoreManager" 
+        type="org.apache.tapestry.contrib.table.model.ITableSessionStoreManager" 
+        required="no">
+        <description>
+            The manager that controls where the session data will be stored.
+        </description>
+    </parameter>
+    
+    <parameter name="columnSettingsContainer"
+        type="org.apache.tapestry.IComponent"
+        required="no"
+        default-value="container">
+        <description>
+            The component where Block and messages are pulled from when using source/columns.
+        </description>
+    </parameter>
+
+	<parameter name="pagesDisplayed" 
+		type="int" 
+		required="no">
+        <description>
+            The maximum number of pages that will be displayed in the list of table pages.
+            By default, only seven of the pages around the current one are shown.
+        </description>
+    </parameter>
+  
+	<parameter name="column" 
+		type="org.apache.tapestry.contrib.table.model.ITableColumn" 
+		required="no">
+        <description>
+            The column that is being rendered. This value is updated when both 
+            the column headers and column values are rendered.
+        </description>
+    </parameter>
+
+	<parameter name="row" 
+		type="Object" 
+		required="no"
+		direction="custom">
+        <description>
+            The row that is being rendered. This value is null when 
+            the column headers are rendered.
+        </description>
+    </parameter>
+
+	<parameter name="arrowUpAsset" 
+		type="org.apache.tapestry.IAsset" 
+		required="no"
+		direction="in">
+        <description>
+            The image to use to describe a column sorted in an ascending order.
+        </description>
+    </parameter>
+
+	<parameter name="arrowDownAsset" 
+		type="org.apache.tapestry.IAsset" 
+		required="no"
+		direction="in">
+        <description>
+            The image to use to describe a column sorted in a descending order.
+        </description>
+    </parameter>
+
+	<parameter name="pagesClass" 
+		type="java.lang.String" 
+		required="no"
+		direction="custom">
+        <description>The CSS class of the table pages</description>
+    </parameter>
+  
+	<parameter name="columnsClass" 
+		type="java.lang.String" 
+		required="no"
+		direction="custom">
+        <description>The CSS class of the table columns</description>
+    </parameter>
+  
+	<parameter name="rowsClass" 
+		type="java.lang.String" 
+		required="no"
+		direction="custom">
+        <description>The CSS class of the table rows</description>
+    </parameter>
+  
+	<parameter name="valuesClass" 
+		type="java.lang.String" 
+		required="no"
+		direction="custom">
+        <description>The CSS class of the table values</description>
+    </parameter>
+  
+
+	<component id="tableElement" type="Any" inherit-informal-parameters="yes">
+		<static-binding name="element">table</static-binding>
+	</component>
+
+	<component id="condPages" type="Conditional">
+		<binding name="condition" expression="tableModel.pageCount > 1"/>
+	</component>
+
+
+	<component id="tableView" type="TableView">
+		<inherited-binding name="tableModel" parameter-name="tableModel"/>
+		<inherited-binding name="source" parameter-name="source"/>
+		<inherited-binding name="columns" parameter-name="columns"/>
+		<inherited-binding name="pageSize" parameter-name="pageSize"/>
+		<inherited-binding name="initialSortColumn" parameter-name="initialSortColumn"/>
+		<inherited-binding name="initialSortOrder" parameter-name="initialSortOrder"/>
+		<inherited-binding name="tableSessionStateManager" parameter-name="tableSessionStateManager"/>
+		<inherited-binding name="tableSessionStoreManager" parameter-name="tableSessionStoreManager"/>
+		<inherited-binding name="columnSettingsContainer" parameter-name="columnSettingsContainer"/>
+		<static-binding name="element">span</static-binding>
+	</component>
+
+	<component id="tablePages" type="TablePages">
+		<inherited-binding name="pagesDisplayed" parameter-name="pagesDisplayed"/>
+		<inherited-binding name="class" parameter-name="pagesClass"/>
+	</component>
+
+	<component id="tableColumns" type="TableColumns">
+		<inherited-binding name="column" parameter-name="column"/>
+		<inherited-binding name="class" parameter-name="columnsClass"/>
+        <inherited-binding name="arrowUpAsset" parameter-name="arrowUpAsset"/>
+        <inherited-binding name="arrowDownAsset" parameter-name="arrowDownAsset"/>
+	</component>
+
+	<component id="tableRows" type="TableRows">
+		<inherited-binding name="row" parameter-name="row"/>
+		<inherited-binding name="class" parameter-name="rowsClass"/>
+	</component>
+
+	<component id="tableValues" type="TableValues">
+		<inherited-binding name="column" parameter-name="column"/>
+		<inherited-binding name="class" parameter-name="valuesClass"/>
+	</component>
+  
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableColumns.html b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableColumns.html
new file mode 100644
index 0000000..21360fd
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableColumns.html
@@ -0,0 +1,9 @@
+<!-- $Id$ -->
+
+<span jwcid="$content$">
+
+<span jwcid="iterColumns">
+	<th jwcid="informal"><span jwcid="insertColumnRenderer"/></th>
+</span>
+
+</span>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableColumns.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableColumns.java
new file mode 100644
index 0000000..06c90e8
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableColumns.java
@@ -0,0 +1,149 @@
+//  Copyright 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.tapestry.contrib.table.components;
+
+import java.util.Iterator;
+
+import org.apache.tapestry.IAsset;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+import org.apache.tapestry.contrib.table.model.ITableColumnModel;
+
+/**
+ * A low level Table component that renders the column headers in the table.
+ * This component must be wrapped by {@link org.apache.tapestry.contrib.table.components.TableView}.
+ * <p>
+ * The component iterates over all column objects in the
+ * {@link org.apache.tapestry.contrib.table.model.ITableColumnModel} and renders
+ * a header for each one of them using the renderer provided by the
+ * getColumnRender() method in {@link org.apache.tapestry.contrib.table.model.ITableColumn}.
+ * The headers are wrapped in 'th' tags by default.
+ * <p>
+ * Please see the Component Reference for details on how to use this component. 
+ * 
+ *  [<a href="../../../../../../../ComponentReference/contrib.TableColumns.html">Component Reference</a>]
+ * 
+ * @author mindbridge
+ * @version $Id$
+ *
+ */
+public abstract class TableColumns extends AbstractTableViewComponent
+{
+    public static final String TABLE_COLUMN_ARROW_UP_ATTRIBUTE =
+        "org.apache.tapestry.contrib.table.components.TableColumns.arrowUp";
+
+    public static final String TABLE_COLUMN_ARROW_DOWN_ATTRIBUTE =
+        "org.apache.tapestry.contrib.table.components.TableColumns.arrowDown";
+
+    public static final String TABLE_COLUMN_CSS_CLASS_SUFFIX = "ColumnHeader";
+
+    // Bindings
+    public abstract IBinding getColumnBinding();
+    public abstract IBinding getClassBinding();
+    public abstract IAsset getArrowDownAsset();
+    public abstract IAsset getArrowUpAsset();
+
+    // Transient
+    private ITableColumn m_objTableColumn = null;
+
+    /**
+     * Returns the currently rendered table column. 
+     * You can call this method to obtain the current column.
+     *  
+     * @return ITableColumn the current table column
+     */
+    public ITableColumn getTableColumn()
+    {
+        return m_objTableColumn;
+    }
+
+    /**
+     * Sets the currently rendered table column. 
+     * This method is for internal use only.
+     * 
+     * @param tableColumn The current table column
+     */
+    public void setTableColumn(ITableColumn tableColumn)
+    {
+        m_objTableColumn = tableColumn;
+
+        IBinding objColumnBinding = getColumnBinding();
+        if (objColumnBinding != null)
+            objColumnBinding.setObject(tableColumn);
+    }
+
+    /**
+     * Get the list of all table columns to be displayed.
+     * 
+     * @return an iterator of all table columns
+     */
+    public Iterator getTableColumnIterator()
+    {
+        ITableColumnModel objColumnModel = getTableModelSource().getTableModel().getColumnModel();
+        return objColumnModel.getColumns();
+    }
+
+    /**
+     * Returns the renderer to be used to generate the header of the current column
+     * 
+     * @return the header renderer of the current column
+     */
+    public IRender getTableColumnRenderer()
+    {
+        return getTableColumn().getColumnRenderer(
+            getPage().getRequestCycle(),
+            getTableModelSource());
+    }
+
+    /**
+     * Returns the CSS class of the generated table cell.
+     * It uses the class parameter if it has been bound, or
+     * the default value of "[column name]ColumnHeader" otherwise.
+     * 
+     * @return the CSS class of the cell
+     */
+    public String getColumnClass()
+    {
+        IBinding objClassBinding = getClassBinding();
+        if (objClassBinding != null)
+            return objClassBinding.getString();
+        else
+            return getTableColumn().getColumnName() + TABLE_COLUMN_CSS_CLASS_SUFFIX;
+    }
+
+    /**
+     * @see org.apache.tapestry.BaseComponent#renderComponent(IMarkupWriter, IRequestCycle)
+     */
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        Object oldValueUp = cycle.getAttribute(TABLE_COLUMN_ARROW_UP_ATTRIBUTE);
+        Object oldValueDown = cycle.getAttribute(TABLE_COLUMN_ARROW_DOWN_ATTRIBUTE);
+
+        cycle.setAttribute(TABLE_COLUMN_ARROW_UP_ATTRIBUTE, getArrowUpAsset());
+        cycle.setAttribute(TABLE_COLUMN_ARROW_DOWN_ATTRIBUTE, getArrowDownAsset());
+
+        super.renderComponent(writer, cycle);
+
+        cycle.setAttribute(TABLE_COLUMN_ARROW_UP_ATTRIBUTE, oldValueUp);
+        cycle.setAttribute(TABLE_COLUMN_ARROW_DOWN_ATTRIBUTE, oldValueDown);
+
+        // set the current column to null when the component is not active
+        m_objTableColumn = null;
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableColumns.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableColumns.jwc
new file mode 100644
index 0000000..37e5757
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableColumns.jwc
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!--  $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.contrib.table.components.TableColumns" 
+	allow-body="yes" allow-informal-parameters="yes">
+	
+	<description>
+        A low level Table component that renders the column headers in the table. 
+        This component must be wrapped by TableView. 
+	</description>
+  
+	<parameter name="column" 
+		type="org.apache.tapestry.contrib.table.model.ITableColumn" 
+		required="no"
+		direction="custom">
+        <description>The column currently being rendered [out]</description>
+    </parameter>
+
+	<parameter name="element" 
+		type="java.lang.String" 
+		required="no"
+		direction="auto"
+    	default-value="'th'">
+        <description>The tag to use to wrap the column headers.</description>
+    </parameter>
+
+	<parameter name="arrowUpAsset" 
+		type="org.apache.tapestry.IAsset" 
+		required="no"
+		direction="in">
+        <description>The image to use to describe a column sorted in an ascending order.</description>
+    </parameter>
+
+	<parameter name="arrowDownAsset" 
+		type="org.apache.tapestry.IAsset" 
+		required="no"
+		direction="in">
+        <description>The image to use to describe a column sorted in a descending order.</description>
+    </parameter>
+
+    <parameter name="class"
+        type="java.lang.String"
+        required="no"
+        direction="custom">
+        <description>The CSS class of the table columns</description>
+    </parameter>
+    
+	<component id="iterColumns" type="Foreach">
+		<binding name="source" expression="tableColumnIterator"/>
+		<binding name="value" expression="tableColumn"/>
+	</component>
+  
+	<component id="informal" type="Any" inherit-informal-parameters="yes">
+		<binding name="element" expression="element"/>
+        <binding name="class" expression="columnClass"/>
+	</component>
+  
+	<component id="insertColumnRenderer" type="Delegator">
+		<binding name="delegate" expression="tableColumnRenderer"/>
+	</component>
+
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableFormPages.html b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableFormPages.html
new file mode 100644
index 0000000..1d98cd8
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableFormPages.html
@@ -0,0 +1,32 @@
+<!-- $Id$ -->
+
+<span jwcid="$content$">
+
+<span jwcid="informal">
+
+  <span jwcid="hiddenCurrentPage"/>
+  <span jwcid="hiddenPageCount"/>
+  <span jwcid="hiddenStartPage"/>
+  <span jwcid="hiddenStopPage"/>
+
+  <a jwcid="linkFirst">&lt;&lt;</a>  
+  <a jwcid="linkBack">&lt;</a> 
+
+  <span jwcid="iterPage">
+
+	  <span jwcid="condCurrent">
+		  <b><span jwcid="insertCurrentPage"/></b>
+	  </span>
+
+    <span jwcid="condOther">
+		  <a jwcid="linkPage"><span jwcid="insertOtherPage"/></a>
+	  </span>
+
+  </span>
+
+  <a jwcid="linkFwd">&gt;</a>
+  <a jwcid="linkLast">&gt;&gt;</a>
+
+</span>
+
+</span>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableFormPages.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableFormPages.java
new file mode 100644
index 0000000..2043799
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableFormPages.java
@@ -0,0 +1,173 @@
+//  Copyright 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.tapestry.contrib.table.components;
+
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.table.model.ITableModelSource;
+import org.apache.tapestry.event.PageDetachListener;
+import org.apache.tapestry.event.PageEvent;
+import org.apache.tapestry.event.PageRenderListener;
+
+/**
+ * A low level Table component that renders the pages in the table.
+ * 
+ * This component is a variant of {@link org.apache.tapestry.contrib.table.components.TablePages}, 
+ * but is designed for operation in a form. The necessary page data is stored 
+ * in hidden fields, so that no StaleLink exceptions occur during a rewind. 
+ * The links also submit the form, which ensures that the data in the other 
+ * form fields is preserved even when the page chages.
+ *  
+ * The component must be wrapped by {@link org.apache.tapestry.contrib.table.components.TableView}.
+ * <p>
+ * The component generates a list of pages in the Table centered around the 
+ * current one and allows you to navigate to other pages.
+ * <p> 
+ * Please see the Component Reference for details on how to use this component. 
+ * 
+ *  [<a href="../../../../../../../ComponentReference/contrib.TableFormPages.html">Component Reference</a>]
+ * 
+ * @author mindbridge
+ * @version $Id$
+ *
+ */
+public abstract class TableFormPages extends TablePages 
+    implements PageDetachListener, PageRenderListener
+{
+    private int m_nCurrentPage;
+    private int m_nPageCount;
+    private int m_nStartPage;
+    private int m_nStopPage;    
+
+    public TableFormPages()
+    {
+        initialize();
+    }
+
+    /**
+     * @see org.apache.tapestry.event.PageDetachListener#pageDetached(org.apache.tapestry.event.PageEvent)
+     */
+    public void pageDetached(PageEvent event)
+    {
+        initialize();
+    }
+    
+    /**
+     * @see org.apache.tapestry.event.PageRenderListener#pageBeginRender(org.apache.tapestry.event.PageEvent)
+     */
+    public void pageBeginRender(PageEvent event)
+    {
+        // values set during rewind are removed
+        initialize();
+    }
+
+    /**
+     * Initialize the values and return the object to operation identical
+     * to that of the super class.
+     */
+    private void initialize()
+    {
+        m_nCurrentPage = -1;
+        m_nPageCount = -1;
+        m_nStartPage = -1;
+        m_nStopPage = -1;
+    }
+
+    // This would ideally be a delayed invocation -- called after the form rewind
+    public void changePage(IRequestCycle objCycle)
+    {
+        ITableModelSource objSource = getTableModelSource(); 
+        setCurrentPage(objSource, getSelectedPage());
+
+        // ensure that the change is saved
+        objSource.fireObservedStateChange();
+    }
+
+    // defined in the JWC file
+    public abstract int getSelectedPage();
+
+
+    /**
+     * @return the current page
+     */
+    public int getCurrentPage()
+    {
+        if (m_nCurrentPage < 0)
+            m_nCurrentPage = super.getCurrentPage();
+        return m_nCurrentPage;
+    }
+
+    /**
+     * @return number of all pages to display
+     */
+    public int getPageCount()
+    {
+        if (m_nPageCount < 0)
+            m_nPageCount = super.getPageCount();
+        return m_nPageCount;
+    }
+
+    /**
+     * @return the first page to display
+     */
+    public int getStartPage()
+    {
+        if (m_nStartPage < 0)
+            m_nStartPage = super.getStartPage();
+        return m_nStartPage;
+    }
+
+    /**
+     * @return the last page to display
+     */
+    public int getStopPage()
+    {
+        if (m_nStopPage < 0)
+            m_nStopPage = super.getStopPage();
+        return m_nStopPage;
+    }
+
+    /**
+     * @param i the current page
+     */
+    public void setCurrentPage(int i)
+    {
+        m_nCurrentPage = i;
+    }
+
+    /**
+     * @param i number of all pages to display
+     */
+    public void setPageCount(int i)
+    {
+        m_nPageCount = i;
+    }
+
+    /**
+     * @param i the first page to display
+     */
+    public void setStartPage(int i)
+    {
+        m_nStartPage = i;
+    }
+
+    /**
+     * @param i the last page to display
+     */
+    public void setStopPage(int i)
+    {
+        m_nStopPage = i;
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableFormPages.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableFormPages.jwc
new file mode 100644
index 0000000..a4bfe7d
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableFormPages.jwc
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!--  $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://tapestry.apache.org/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.contrib.table.components.TableFormPages"
+                         allow-body="no" allow-informal-parameters="yes">
+	
+	<description>
+        A version of TablePages that is designed for operation in a form. 
+        It is a low level Table component that renders the pages in the table. 
+        This component must be wrapped by TableView. 
+	</description>
+  
+	<parameter name="pagesDisplayed" 
+		type="int" 
+		required="no"
+		direction="auto"
+    	default-value="7">
+		<description>
+            Determines the maximum number of pages to be displayed in the page list 
+            when the table has more than one page. 
+        </description>
+    </parameter>
+  
+	<property-specification name="selectedPage" type="int"/>
+
+    <component id="informal" type="Any" inherit-informal-parameters="yes"/>
+
+    <component id="hiddenCurrentPage" type="Hidden">
+		<binding name="value" expression="currentPage"/>
+	</component>
+  
+	<component id="hiddenPageCount" type="Hidden">
+		<binding name="value" expression="pageCount"/>
+	</component>
+  
+	<component id="hiddenStartPage" type="Hidden">
+		<binding name="value" expression="startPage"/>
+	</component>
+  
+	<component id="hiddenStopPage" type="Hidden">
+		<binding name="value" expression="stopPage"/>
+	</component>
+  
+	<component id="condCurrent" type="Conditional">
+		<binding name="condition" expression="condCurrent"/>
+	</component>
+  
+	<component id="condOther" type="Conditional">
+		<binding name="condition" expression="!condCurrent"/>
+	</component>
+  
+	<component id="iterPage" type="Foreach">
+		<binding name="source" expression="pageList"/>
+		<binding name="value" expression="displayPage"/>
+	</component>
+
+	<component id="insertCurrentPage" type="Insert">
+	    <binding name="value" expression="displayPage"/>
+	</component>
+
+	<component id="insertOtherPage" type="Insert">
+	    <binding name="value" expression="displayPage"/>
+	</component>
+
+	<component id="linkPage" type="LinkSubmit">
+	    <binding name="listener" expression="listeners.changePage"/>
+	    <binding name="tag" expression="displayPage"/>
+	    <binding name="selected" expression="selectedPage"/>
+	</component>
+
+	<component id="linkFirst" type="LinkSubmit">
+	    <binding name="listener" expression="listeners.changePage"/>
+	    <binding name="tag" expression="1"/>
+	    <binding name="selected" expression="selectedPage"/>
+	    <binding name="disabled" expression="!condBack"/>
+	</component>
+
+	<component id="linkBack" type="LinkSubmit">
+	    <binding name="listener" expression="listeners.changePage"/>
+	    <binding name="tag" expression="currentPage - 1"/>
+	    <binding name="selected" expression="selectedPage"/>
+	    <binding name="disabled" expression="!condBack"/>
+	</component>
+
+	<component id="linkFwd" type="LinkSubmit">
+	    <binding name="listener" expression="listeners.changePage"/>
+	    <binding name="tag" expression="currentPage + 1"/>
+	    <binding name="selected" expression="selectedPage"/>
+	    <binding name="disabled" expression="!condFwd"/>
+	</component>
+	
+	<component id="linkLast" type="LinkSubmit">
+	    <binding name="listener" expression="listeners.changePage"/>
+	    <binding name="tag" expression="pageCount"/>
+	    <binding name="selected" expression="selectedPage"/>
+	    <binding name="disabled" expression="!condFwd"/>
+	</component>
+
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableFormRows.html b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableFormRows.html
new file mode 100644
index 0000000..53b492a
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableFormRows.html
@@ -0,0 +1,9 @@
+<!-- $Id$ -->
+
+<span jwcid="$content$">
+
+<tr jwcid="iterRows">
+	<span jwcid="@RenderBody"/>
+</tr>
+
+</span>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableFormRows.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableFormRows.java
new file mode 100644
index 0000000..f33dae9
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableFormRows.java
@@ -0,0 +1,134 @@
+//  Copyright 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.tapestry.contrib.table.components;
+
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.table.model.*;
+
+
+/**
+ * A low level Table component that generates the rows of the current page in the table.
+ * 
+ * This component is a variant of {@link org.apache.tapestry.contrib.table.components.TablePages},
+ * but is designed for operation in a form. The displayed rows are stored in 
+ * hidden form fields, which are then read during a rewind. This ensures that
+ * the form will rewind in exactly the same was as it was rendered even if the 
+ * TableModel has changed and no StaleLink exceptions will occur. 
+ * 
+ * The component must be wrapped by {@link org.apache.tapestry.contrib.table.components.TableView}.
+ * 
+ * <p>
+ * The component iterates over the rows of the current page in the table. 
+ * The rows are wrapped in 'tr' tags by default. 
+ * You can define columns manually within, or
+ * you can use {@link org.apache.tapestry.contrib.table.components.TableValues} 
+ * to generate the columns automatically.
+ * <p> 
+ * Please see the Component Reference for details on how to use this component. 
+ * 
+ *  [<a href="../../../../../../../ComponentReference/contrib.TableFormRows.html">Component Reference</a>]
+ * 
+ * @author mindbridge
+ * @version $Id$
+ *
+ */
+public abstract class TableFormRows extends TableRows
+{
+    public abstract IPrimaryKeyConvertor getConvertor();
+    public abstract IPrimaryKeyConvertor getConvertorCache();
+    public abstract void setConvertorCache(IPrimaryKeyConvertor convertor);
+    public abstract Map getConvertedValues();
+
+    /**
+     * Returns the PK convertor cached within the realm of the current request cycle.
+     *  
+     * @return the cached PK convertor
+     */
+    public IPrimaryKeyConvertor getCachedConvertor()
+    {
+        IPrimaryKeyConvertor objConvertor = getConvertorCache();
+        
+        if (objConvertor == null) {
+            objConvertor = getConvertor();
+            setConvertorCache(objConvertor);
+        }
+        
+        return objConvertor;
+    }
+
+    /**
+     * Get the list of all table rows to be displayed on this page, converted 
+     * using the PK.convertor.
+     * 
+     * @return an iterator of all converted table rows
+     */    
+    public Iterator getConvertedTableRowsIterator()
+    {
+        final Iterator objTableRowsIterator = getTableRowsIterator(); 
+        final IPrimaryKeyConvertor objConvertor = getCachedConvertor();
+        if (objConvertor == null)
+            return objTableRowsIterator;
+            
+        return new Iterator()
+        {
+            public boolean hasNext()
+            {
+                return objTableRowsIterator.hasNext();
+            }
+
+            public Object next()
+            {
+                Object objValue = objTableRowsIterator.next();
+                Object objPrimaryKey = objConvertor.getPrimaryKey(objValue);
+                Map mapConvertedValues = getConvertedValues(); 
+                mapConvertedValues.put(objPrimaryKey, objValue);
+                return objPrimaryKey;
+            }
+
+            public void remove()
+            {
+                objTableRowsIterator.remove();
+            }
+        };
+    }
+
+    /**
+     * Sets the current table row PK and invokes {@link #setTableRow(Object)} as a result.
+     * This method is for internal use only.
+     * 
+     * @param objConvertedTableRow The current converted table row (PK)
+     */
+    public void setConvertedTableRow(Object objConvertedTableRow)
+    {
+        Object objValue = objConvertedTableRow;
+
+        IPrimaryKeyConvertor objConvertor = getCachedConvertor();
+        if (objConvertor != null) {
+            IRequestCycle objCycle = getPage().getRequestCycle();
+            if (objCycle.isRewinding()) {
+                objValue = objConvertor.getValue(objConvertedTableRow);  
+            }
+            else {
+                Map mapConvertedValues = getConvertedValues(); 
+                objValue = mapConvertedValues.get(objConvertedTableRow);
+            }
+        }
+
+        setTableRow(objValue);
+    }
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableFormRows.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableFormRows.jwc
new file mode 100644
index 0000000..30d5992
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableFormRows.jwc
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!--  $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.contrib.table.components.TableFormRows" 
+	allow-body="yes" allow-informal-parameters="yes">
+	
+	<description>
+        A version of the TableRows designed for operation in a form. 
+        This is a low level Table component that generates the rows of 
+        the current page in the table. Each row is stored as a hidden value 
+        in the form, which eliminates the chance of a stale link during rewinding. 
+        This component must be wrapped by TableView. 
+	</description>
+  
+	<parameter name="row" 
+		type="Object" 
+		required="no"
+		direction="custom">
+        <description>The value object of the row currently being rendered.</description>
+    </parameter>
+
+    <parameter name="convertor"
+    	type="org.apache.tapestry.contrib.table.model.IPrimaryKeyConvertor"
+    	required="no"
+    	direction="auto"
+    	default-value="null">
+        <description>
+        An interface defining how the items iterated upon by this component 
+        will be stored in the form as Hidden values. This interface allows only 
+        the primary key of the items to be stored, rather than the whole item.
+        </description>
+    </parameter>
+    
+	<parameter name="element" 
+		type="java.lang.String" 
+		required="no"
+		default-value='"tr"'>
+        <description>The tag to use to wrap the rows in, 'tr' by default.</description>
+    </parameter>
+
+	<component id="iterRows" type="ListEdit" inherit-informal-parameters="yes">
+		<binding name="source" expression="convertedTableRowsIterator"/>
+		<binding name="value" expression="convertedTableRow"/>
+        <inherited-binding name="element" parameter-name="element"/>
+	</component>
+
+    <property-specification name="convertedValues" type="java.util.Map" initial-value="new java.util.HashMap()"/>
+    <property-specification name="convertorCache" type="org.apache.tapestry.contrib.table.model.IPrimaryKeyConvertor" initial-value="null"/>
+      
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TablePages.html b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TablePages.html
new file mode 100644
index 0000000..ecf98ec
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TablePages.html
@@ -0,0 +1,27 @@
+<!-- $Id$ -->
+
+<span jwcid="$content$">
+
+<span jwcid="informal">
+
+	<a jwcid="linkFirst">&lt;&lt;</a>  
+	<a jwcid="linkBack">&lt;</a> 
+	
+	<span jwcid="iterPage">
+	
+		<span jwcid="condCurrent">
+			<b><span jwcid="insertCurrentPage"/></b>
+		</span>
+	
+		<span jwcid="condOther">
+			<a jwcid="linkPage"><span jwcid="insertOtherPage"/></a>
+		</span>
+	
+	</span>
+	
+	<a jwcid="linkFwd">&gt;</a>
+	<a jwcid="linkLast">&gt;&gt;</a>
+
+</span>
+
+</span>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TablePages.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TablePages.java
new file mode 100644
index 0000000..6aee51f
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TablePages.java
@@ -0,0 +1,195 @@
+//  Copyright 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.tapestry.contrib.table.components;
+
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.table.model.ITableModelSource;
+import org.apache.tapestry.util.ComponentAddress;
+
+/**
+ * A low level Table component that renders the pages in the table.
+ * This component must be wrapped by {@link org.apache.tapestry.contrib.table.components.TableView}.
+ * <p>
+ * The component generates a list of pages in the Table centered around the 
+ * current one and allows you to navigate to other pages.
+ * <p> 
+ * Please see the Component Reference for details on how to use this component. 
+ * 
+ *  [<a href="../../../../../../../ComponentReference/contrib.TablePages.html">Component Reference</a>]
+ * 
+ * @author mindbridge
+ * @version $Id$
+ *
+ */
+public abstract class TablePages extends AbstractTableViewComponent
+{
+    // Bindings    
+    public abstract int getPagesDisplayed();
+
+    // Transient
+    private int m_nDisplayPage;
+
+    /**
+     * Returns the displayPage.
+     * @return int
+     */
+    public int getDisplayPage()
+    {
+        return m_nDisplayPage;
+    }
+
+    /**
+     * Sets the displayPage.
+     * @param displayPage The displayPage to set
+     */
+    public void setDisplayPage(int displayPage)
+    {
+        m_nDisplayPage = displayPage;
+    }
+
+    public int getCurrentPage()
+    {
+        return getTableModelSource().getTableModel().getPagingState().getCurrentPage() + 1;
+    }
+
+    public int getPageCount()
+    {
+        return getTableModelSource().getTableModel().getPageCount();
+    }
+
+    public boolean getCondBack()
+    {
+        return getCurrentPage() > 1;
+    }
+
+    public boolean getCondFwd()
+    {
+        return getCurrentPage() < getPageCount();
+    }
+
+    public boolean getCondCurrent()
+    {
+        return getDisplayPage() == getCurrentPage();
+    }
+
+    public int getStartPage()
+    {
+        int nCurrent = getCurrentPage();
+        int nPagesDisplayed = getPagesDisplayed();
+
+        int nRightMargin = nPagesDisplayed / 2;
+        int nStop = nCurrent + nRightMargin;
+        int nLastPage = getPageCount();
+
+        int nLeftAddon = 0;
+        if (nStop > nLastPage)
+            nLeftAddon = nStop - nLastPage;
+
+        int nLeftMargin = (nPagesDisplayed - 1) / 2 + nLeftAddon;
+        int nStart = nCurrent - nLeftMargin;
+        int nFirstPage = 1;
+        if (nStart < nFirstPage)
+            nStart = nFirstPage;
+        return nStart;
+    }
+
+    public int getStopPage()
+    {
+        int nCurrent = getCurrentPage();
+        int nPagesDisplayed = getPagesDisplayed();
+
+        int nLeftMargin = (nPagesDisplayed - 1) / 2;
+        int nStart = nCurrent - nLeftMargin;
+        int nFirstPage = 1;
+
+        int nRightAddon = 0;
+        if (nStart < nFirstPage)
+            nRightAddon = nFirstPage - nStart;
+
+        int nRightMargin = nPagesDisplayed / 2 + nRightAddon;
+        int nStop = nCurrent + nRightMargin;
+        int nLastPage = getPageCount();
+        if (nStop > nLastPage)
+            nStop = nLastPage;
+        return nStop;
+    }
+
+    public Integer[] getPageList()
+    {
+        int nStart = getStartPage();
+        int nStop = getStopPage();
+
+        Integer[] arrPages = new Integer[nStop - nStart + 1];
+        for (int i = nStart; i <= nStop; i++)
+            arrPages[i - nStart] = new Integer(i);
+
+        return arrPages;
+    }
+
+    public Object[] getFirstPageContext()
+    {
+        ComponentAddress objAddress = new ComponentAddress(getTableModelSource());
+        return new Object[] { objAddress, new Integer(1)};
+    }
+
+    public Object[] getLastPageContext()
+    {
+        ComponentAddress objAddress = new ComponentAddress(getTableModelSource());
+        return new Object[] { objAddress, new Integer(getPageCount())};
+    }
+
+    public Object[] getBackPageContext()
+    {
+        ComponentAddress objAddress = new ComponentAddress(getTableModelSource());
+        return new Object[] { objAddress, new Integer(getCurrentPage() - 1)};
+    }
+
+    public Object[] getFwdPageContext()
+    {
+        ComponentAddress objAddress = new ComponentAddress(getTableModelSource());
+        return new Object[] { objAddress, new Integer(getCurrentPage() + 1)};
+    }
+
+    public Object[] getDisplayPageContext()
+    {
+        ComponentAddress objAddress = new ComponentAddress(getTableModelSource());
+        return new Object[] { objAddress, new Integer(m_nDisplayPage)};
+    }
+
+    public void changePage(IRequestCycle objCycle)
+    {
+        Object[] arrParameters = objCycle.getServiceParameters();
+        if (arrParameters.length != 2
+            && !(arrParameters[0] instanceof ComponentAddress)
+            && !(arrParameters[1] instanceof Integer))
+        {
+            // error
+            return;
+        }
+
+        ComponentAddress objAddress = (ComponentAddress) arrParameters[0];
+        ITableModelSource objSource = (ITableModelSource) objAddress.findComponent(objCycle);
+        setCurrentPage(objSource, ((Integer) arrParameters[1]).intValue());
+
+        // ensure that the change is saved
+        objSource.fireObservedStateChange();
+    }
+
+    public void setCurrentPage(ITableModelSource objSource, int nPage)
+    {
+        objSource.getTableModel().getPagingState().setCurrentPage(nPage - 1);
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TablePages.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TablePages.jwc
new file mode 100644
index 0000000..98640de
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TablePages.jwc
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!--  $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.contrib.table.components.TablePages" 
+	allow-body="no" allow-informal-parameters="yes">
+	
+	<description>
+        A low level Table component that renders the pages in the table. 
+        This component must be wrapped by TableView. 
+	</description>
+  
+	<parameter name="pagesDisplayed" 
+		type="int" 
+		required="no"
+		direction="auto"
+    	default-value="7">
+		<description>
+            Determines the maximum number of pages to be displayed in the page list 
+            when the table has more than one page. 
+        </description>
+    </parameter>
+
+	<component id="informal" type="Any" inherit-informal-parameters="yes"/>
+  
+	<component id="condCurrent" type="Conditional">
+		<binding name="condition" expression="condCurrent"/>
+	</component>
+  
+	<component id="condOther" type="Conditional">
+		<binding name="condition" expression="condCurrent"/>
+		<static-binding name="invert">true</static-binding>
+	</component>
+  
+	<component id="iterPage" type="Foreach">
+		<binding name="source" expression="pageList"/>
+		<binding name="value" expression="displayPage"/>
+	</component>
+
+	<component id="insertCurrentPage" type="Insert">
+	    <binding name="value" expression="displayPage"/>
+	</component>
+
+	<component id="insertOtherPage" type="Insert">
+	    <binding name="value" expression="displayPage"/>
+	</component>
+
+	<component id="linkPage" type="DirectLink">
+	    <binding name="listener" expression="listeners.changePage"/>
+	    <binding name="parameters" expression="displayPageContext"/>
+	</component>
+
+	<component id="linkFirst" type="DirectLink">
+	    <binding name="listener" expression="listeners.changePage"/>
+	    <binding name="parameters" expression="firstPageContext"/>
+	    <binding name="disabled" expression="!condBack"/>
+	</component>
+
+	<component id="linkBack" type="DirectLink">
+	    <binding name="listener" expression="listeners.changePage"/>
+	    <binding name="parameters" expression="backPageContext"/>
+	    <binding name="disabled" expression="!condBack"/>
+	</component>
+
+	<component id="linkFwd" type="DirectLink">
+	    <binding name="listener" expression="listeners.changePage"/>
+	    <binding name="parameters" expression="fwdPageContext"/>
+	    <binding name="disabled" expression="!condFwd"/>
+	</component>
+	
+	<component id="linkLast" type="DirectLink">
+	    <binding name="listener" expression="listeners.changePage"/>
+	    <binding name="parameters" expression="lastPageContext"/>
+	    <binding name="disabled" expression="!condFwd"/>
+	</component>
+	
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableRows.html b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableRows.html
new file mode 100644
index 0000000..8a73f34
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableRows.html
@@ -0,0 +1,11 @@
+<!-- $Id$ -->
+
+<span jwcid="$content$">
+
+<span jwcid="iterRows">
+	<tr jwcid="informal">
+		<span jwcid="wrapped"/>
+	</tr>
+</span>
+
+</span>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableRows.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableRows.java
new file mode 100644
index 0000000..b96b427
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableRows.java
@@ -0,0 +1,106 @@
+//  Copyright 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.tapestry.contrib.table.components;
+
+import java.util.Iterator;
+
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.table.model.ITableModel;
+import org.apache.tapestry.contrib.table.model.ITableRowSource;
+
+/**
+ * A low level Table component that generates the rows of the current page in the table.
+ * This component must be wrapped by {@link org.apache.tapestry.contrib.table.components.TableView}.
+ * 
+ * <p>
+ * The component iterates over the rows of the current page in the table. 
+ * The rows are wrapped in 'tr' tags by default. 
+ * You can define columns manually within, or
+ * you can use {@link org.apache.tapestry.contrib.table.components.TableValues} 
+ * to generate the columns automatically.
+ * 
+ * <p> 
+ * Please see the Component Reference for details on how to use this component. 
+ * 
+ *  [<a href="../../../../../../../ComponentReference/contrib.TableRows.html">Component Reference</a>]
+ * 
+ * @author mindbridge
+ * @version $Id$
+ *
+ */
+public abstract class TableRows extends AbstractTableViewComponent implements ITableRowSource
+{
+    // Parameters
+    public abstract IBinding getRowBinding();
+
+    // Transient
+    private Object m_objTableRow = null;
+
+    /**
+     * Returns the currently rendered table row.
+     * You can call this method to obtain the current row.
+     *  
+     * @return Object the current table row
+     */
+    public Object getTableRow()
+    {
+        return m_objTableRow;
+    }
+
+    /**
+     * Sets the currently rendered table row. 
+     * This method is for internal use only.
+     * 
+     * @param tableRow The current table row
+     */
+    public void setTableRow(Object tableRow)
+    {
+        m_objTableRow = tableRow;
+
+        IBinding objRowBinding = getRowBinding();
+        if (objRowBinding != null)
+            objRowBinding.setObject(tableRow);
+    }
+
+    /**
+     * Get the list of all table rows to be displayed on this page.
+     * 
+     * @return an iterator of all table rows
+     */
+    public Iterator getTableRowsIterator()
+    {
+        ITableModel objTableModel = getTableModelSource().getTableModel();
+        return objTableModel.getCurrentPageRows();
+    }
+
+    /**
+     * @see org.apache.tapestry.BaseComponent#renderComponent(IMarkupWriter, IRequestCycle)
+     */
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        Object objOldValue = cycle.getAttribute(ITableRowSource.TABLE_ROW_SOURCE_ATTRIBUTE);
+        cycle.setAttribute(ITableRowSource.TABLE_ROW_SOURCE_ATTRIBUTE, this);
+
+        super.renderComponent(writer, cycle);
+
+        cycle.setAttribute(ITableRowSource.TABLE_ROW_SOURCE_ATTRIBUTE, objOldValue);
+
+        // set the current row to null when the component is not active
+        m_objTableRow = null;
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableRows.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableRows.jwc
new file mode 100644
index 0000000..0220ff7
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableRows.jwc
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!--  $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.contrib.table.components.TableRows" 
+	allow-body="yes" allow-informal-parameters="yes">
+	
+	<description>
+        A low level Table component that generates the rows of the current page in the table. 
+        This component must be wrapped by TableView. 
+	</description>
+  
+	<parameter name="row" 
+		type="Object" 
+		required="no"
+		direction="custom">
+        <description>The current row being rendered.</description>
+    </parameter>
+
+	<parameter name="element" 
+		type="java.lang.String" 
+		required="no"
+		direction="auto"
+    	default-value="'tr'">
+        <description>The tag to use to wrap the rows in, 'tr' by default.</description>
+    </parameter>
+
+	<component id="iterRows" type="Foreach">
+		<binding name="source" expression="tableRowsIterator"/>
+		<binding name="value" expression="tableRow"/>
+	</component>
+  
+	<component id="informal" type="Any" inherit-informal-parameters="yes">
+		<inherited-binding name="element" parameter-name="element"/>
+	</component>
+  
+	<component id="wrapped" type="RenderBody"/>
+  
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableStrings.properties b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableStrings.properties
new file mode 100644
index 0000000..71b81e5
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableStrings.properties
@@ -0,0 +1,7 @@
+# $Id$
+
+missing-table-model=Either the 'tableModel' parameter or both 'source' and 'columns' parameters must be specified by component {0}
+columns-only-please=The 'columns' parameter of component {0} must contain a list of ITableColumn objects only
+not-a-column=The expression '{1}' in the 'columns' parameter of component {0} does not evaluate to an ITableColumn
+invalid-table-source=The source parameter of component {0} is of type {1}, but must be of type Object[], Collection, Iterator, or IBasicTableModel
+invalid-table-columns=The columns parameter of component {0} is of type {1}, but must be of type String, ITableColumnModel, ITableColumn[], List, or Iterator
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableUtils.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableUtils.java
new file mode 100644
index 0000000..c7b5bb7
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableUtils.java
@@ -0,0 +1,191 @@
+//  Copyright 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.tapestry.contrib.table.components;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ResourceBundle;
+import java.util.StringTokenizer;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+import org.apache.tapestry.contrib.table.model.ITableColumnModel;
+import org.apache.tapestry.contrib.table.model.ognl.ExpressionTableColumn;
+import org.apache.tapestry.contrib.table.model.simple.SimpleTableColumn;
+import org.apache.tapestry.contrib.table.model.simple.SimpleTableColumnModel;
+import org.apache.tapestry.util.prop.OgnlUtils;
+
+/**
+ *  A placeholder for a static methods related to the Table component
+ *
+ *  @since 3.0
+ *  @version $Id$
+ *  @author Mindbridge
+ **/
+public class TableUtils
+{
+
+    /**
+     *  Contains strings loaded from TableStrings.properties.
+     *
+     **/
+
+    private static ResourceBundle s_objStrings = null;
+
+    /**
+     *  Gets a string from the TableStrings resource bundle.
+     *
+     **/
+
+    public static String format(String key, Object[] args)
+    {
+            synchronized (TableUtils.class) {
+                if (s_objStrings == null)
+                    s_objStrings = ResourceBundle.getBundle("org.apache.tapestry.contrib.table.components.TableStrings");
+            }
+
+        String pattern = s_objStrings.getString(key);
+
+        if (args == null)
+            return pattern;
+
+        return MessageFormat.format(pattern, args);
+    }
+
+    /**
+     *  Convienience method for invoking {@link #format(String, Object[])}.
+     **/
+
+    public static String getMessage(String key)
+    {
+        return format(key, null);
+    }
+
+    /**
+     *  Convienience method for invoking {@link #format(String, Object[])}.
+     **/
+
+    public static String format(String key, Object arg)
+    {
+        return format(key, new Object[] { arg });
+    }
+
+    /**
+     *  Convienience method for invoking {@link #format(String, Object[])}.
+     **/
+
+    public static String format(String key, Object arg1, Object arg2)
+    {
+        return format(key, new Object[] { arg1, arg2 });
+    }
+
+    /**
+     *  Convienience method for invoking {@link #format(String, Object[])}.
+     **/
+
+    public static String format(String key, Object arg1, Object arg2, Object arg3)
+    {
+        return format(key, new Object[] { arg1, arg2, arg3 });
+    }
+
+    /**
+     *  Generate a table column model out of the description string provided.
+     *  Entries in the description string are separated by commas.
+     *  Each column entry is of the format name, name:expression, 
+     *  or name:displayName:expression.
+     *  An entry prefixed with ! represents a non-sortable column.
+     *  If the whole description string is prefixed with *, it represents
+     *  columns to be included in a Form. 
+     * 
+     *  @param strDesc the description of the column model to be generated
+     *  @param objComponent the component ordering the generation
+     *  @param objColumnSettingsContainer the component containing the column settings
+     *  @return a table column model based on the provided parameters
+     */
+    public static ITableColumnModel generateTableColumnModel(String strDesc, IComponent objComponent, IComponent objColumnSettingsContainer)
+    {
+        if (strDesc == null)
+            return null;
+
+        List arrColumns = new ArrayList();
+
+        boolean bFormColumns = false;
+        while (strDesc.startsWith("*"))
+        {
+            strDesc = strDesc.substring(1);
+            bFormColumns = true;
+        }
+
+        StringTokenizer objTokenizer = new StringTokenizer(strDesc, ",");
+        while (objTokenizer.hasMoreTokens())
+        {
+            String strToken = objTokenizer.nextToken().trim();
+
+            if (strToken.startsWith("="))
+            {
+                String strColumnExpression = strToken.substring(1);
+                IResourceResolver objResolver = objColumnSettingsContainer.getPage().getEngine().getResourceResolver();
+
+                Object objColumn =
+                    OgnlUtils.get(strColumnExpression, objResolver, objColumnSettingsContainer);
+                if (!(objColumn instanceof ITableColumn))
+                    throw new ApplicationRuntimeException(
+                        format("not-a-column", objComponent.getExtendedId(), strColumnExpression));
+
+                arrColumns.add(objColumn);
+                continue;
+            }
+
+            boolean bSortable = true;
+            if (strToken.startsWith("!"))
+            {
+                strToken = strToken.substring(1);
+                bSortable = false;
+            }
+
+            StringTokenizer objColumnTokenizer = new StringTokenizer(strToken, ":");
+
+            String strName = "";
+            if (objColumnTokenizer.hasMoreTokens())
+                strName = objColumnTokenizer.nextToken();
+
+            String strExpression = strName;
+            if (objColumnTokenizer.hasMoreTokens())
+                strExpression = objColumnTokenizer.nextToken();
+
+            String strDisplayName = strName;
+            if (objColumnTokenizer.hasMoreTokens())
+            {
+                strDisplayName = strExpression;
+                strExpression = objColumnTokenizer.nextToken();
+            }
+
+            ExpressionTableColumn objColumn =
+                new ExpressionTableColumn(strName, strDisplayName, strExpression, bSortable);
+            if (bFormColumns)
+                objColumn.setColumnRendererSource(SimpleTableColumn.FORM_COLUMN_RENDERER_SOURCE);
+            if (objColumnSettingsContainer != null)
+                objColumn.loadSettings(objColumnSettingsContainer);
+
+            arrColumns.add(objColumn);
+        }
+
+        return new SimpleTableColumnModel(arrColumns);
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableValues.html b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableValues.html
new file mode 100644
index 0000000..29e6927
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableValues.html
@@ -0,0 +1,9 @@
+<!-- $Id$ -->
+
+<span jwcid="$content$">
+
+<span jwcid="iterColumns">
+	<td jwcid="informal"><span jwcid="insertValueRenderer"/></td>
+</span>
+
+</span>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableValues.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableValues.java
new file mode 100644
index 0000000..a316d31
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableValues.java
@@ -0,0 +1,136 @@
+//  Copyright 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.tapestry.contrib.table.components;
+
+import java.util.Iterator;
+
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+import org.apache.tapestry.contrib.table.model.ITableColumnModel;
+
+/**
+ * A low level Table component that generates the columns in the current row in the table.
+ * This component must be wrapped by {@link org.apache.tapestry.contrib.table.components.TableRows}.
+ * 
+ * <p>
+ * The component iterates over the columns in the table and 
+ * automatically renders the column values for the current table row. 
+ * The columns are wrapped in 'td' tags by default. <br>
+ * The column values are rendered using the renderer returned by the 
+ * getValueRenderer() method in {@link org.apache.tapestry.contrib.table.model.ITableColumn}.
+ * 
+ * <p> 
+ * Please see the Component Reference for details on how to use this component. 
+ * 
+ *  [<a href="../../../../../../../ComponentReference/contrib.TableValues.html">Component Reference</a>]
+ * 
+ * @author mindbridge
+ * @version $Id$
+ *
+ */
+public abstract class TableValues extends AbstractTableRowComponent
+{
+    public static final String TABLE_VALUE_CSS_CLASS_SUFFIX = "ColumnValue";
+
+    // Bindings
+    public abstract IBinding getColumnBinding();
+    public abstract IBinding getClassBinding();
+
+	// Transient
+	private ITableColumn m_objTableColumn;
+
+    /**
+     * Get the list of all table columns to be displayed.
+     * 
+     * @return an iterator of all table columns
+     */
+	public Iterator getTableColumnIterator()
+	{
+		ITableColumnModel objColumnModel =
+			getTableModelSource().getTableModel().getColumnModel();
+		return objColumnModel.getColumns();
+	}
+
+    /**
+     * Returns the currently rendered table column. 
+     * You can call this method to obtain the current column.
+     *  
+     * @return ITableColumn the current table column
+     */
+	public ITableColumn getTableColumn()
+	{
+		return m_objTableColumn;
+	}
+
+    /**
+     * Sets the currently rendered table column. 
+     * This method is for internal use only.
+     * 
+     * @param tableColumn The current table column
+     */
+	public void setTableColumn(ITableColumn tableColumn)
+	{
+		m_objTableColumn = tableColumn;
+        
+        IBinding objColumnBinding = getColumnBinding();
+        if (objColumnBinding != null)
+            objColumnBinding.setObject(tableColumn);
+	}
+
+    /**
+     * Returns the renderer to be used to generate the appearance of the current column
+     * 
+     * @return the value renderer of the current column
+     */
+	public IRender getTableValueRenderer()
+	{
+		Object objRow = getTableRowSource().getTableRow();
+		return getTableColumn().getValueRenderer(
+			getPage().getRequestCycle(),
+			getTableModelSource(),
+			objRow);
+	}
+
+    /**
+     * Returns the CSS class of the generated table cell.
+     * It uses the class parameter if it has been bound, or
+     * the default value of "[column name]ColumnValue" otherwise.
+     * 
+     * @return the CSS class of the cell
+     */
+    public String getValueClass()
+    {
+        IBinding objClassBinding = getClassBinding();
+        if (objClassBinding != null)
+            return objClassBinding.getString();
+        else
+            return getTableColumn().getColumnName() + TABLE_VALUE_CSS_CLASS_SUFFIX;
+    }
+
+    /**
+     * @see org.apache.tapestry.BaseComponent#renderComponent(org.apache.tapestry.IMarkupWriter, org.apache.tapestry.IRequestCycle)
+     */
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        super.renderComponent(writer, cycle);
+
+        // set the current column to null when the component is not active
+        m_objTableColumn = null;
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableValues.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableValues.jwc
new file mode 100644
index 0000000..66c4702
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableValues.jwc
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!--  $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.contrib.table.components.TableValues" 
+	allow-body="yes" allow-informal-parameters="yes">
+	
+	<description>
+        A low level Table component that generates the columns for the current row in the table. 
+        This component must be wrapped by TableRows. 
+	</description>
+  
+	<parameter name="column" 
+		type="org.apache.tapestry.contrib.table.model.ITableColumn" 
+		required="no"
+		direction="custom">
+        <description>The current column being rendered</description>
+    </parameter>
+
+	<parameter name="element" 
+		type="java.lang.String" 
+		required="no"
+		direction="auto"
+    	default-value="'td'">
+        <description>The tag to use to wrap the values in, 'td' by default.</description>
+    </parameter>
+
+    <parameter name="class"
+        type="java.lang.String"
+        required="no"
+        direction="custom">
+        <description>The CSS class of the table values</description>
+    </parameter>
+    
+	<component id="iterColumns" type="Foreach">
+		<binding name="source" expression="tableColumnIterator"/>
+		<binding name="value" expression="tableColumn"/>
+	</component>
+  
+	<component id="informal" type="Any" inherit-informal-parameters="yes">
+		<inherited-binding name="element" parameter-name="element"/>
+        <binding name="class" expression="valueClass"/>
+	</component>
+  
+	<component id="insertValueRenderer" type="Delegator">
+		<binding name="delegate" expression="tableValueRenderer"/>
+	</component>
+  
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableView.html b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableView.html
new file mode 100644
index 0000000..331d131
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableView.html
@@ -0,0 +1,9 @@
+<!-- $Id$ -->
+
+<span jwcid="$content$">
+
+<table jwcid="table">
+	<span jwcid="insertWrapped"/>
+</table>
+
+</span>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableView.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableView.java
new file mode 100644
index 0000000..7b60608
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableView.java
@@ -0,0 +1,477 @@
+//  Copyright 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.tapestry.contrib.table.components;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.table.model.IBasicTableModel;
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+import org.apache.tapestry.contrib.table.model.ITableColumnModel;
+import org.apache.tapestry.contrib.table.model.ITableDataModel;
+import org.apache.tapestry.contrib.table.model.ITableModel;
+import org.apache.tapestry.contrib.table.model.ITableModelSource;
+import org.apache.tapestry.contrib.table.model.ITablePagingState;
+import org.apache.tapestry.contrib.table.model.ITableSessionStateManager;
+import org.apache.tapestry.contrib.table.model.ITableSessionStoreManager;
+import org.apache.tapestry.contrib.table.model.common.BasicTableModelWrap;
+import org.apache.tapestry.contrib.table.model.simple.SimpleListTableDataModel;
+import org.apache.tapestry.contrib.table.model.simple.SimpleTableColumnModel;
+import org.apache.tapestry.contrib.table.model.simple.SimpleTableModel;
+import org.apache.tapestry.contrib.table.model.simple.SimpleTableState;
+import org.apache.tapestry.event.PageDetachListener;
+import org.apache.tapestry.event.PageEvent;
+import org.apache.tapestry.event.PageRenderListener;
+
+/**
+ * A low level Table component that wraps all other low level Table components.
+ * This component carries the {@link org.apache.tapestry.contrib.table.model.ITableModel}
+ * that is used by the other Table components. Please see the documentation of
+ * {@link org.apache.tapestry.contrib.table.model.ITableModel} if you need to know more
+ * about how a table is represented.
+ * <p>
+ * This component also handles the saving of the state of the model using an 
+ * {@link org.apache.tapestry.contrib.table.model.ITableSessionStateManager}
+ * to determine what part of the model is to be saved and an 
+ * {@link  org.apache.tapestry.contrib.table.model.ITableSessionStoreManager}
+ * to determine how to save it.
+ * <p>
+ * Upon the beginning of a new request cycle when the table model is first needed,
+ * the model is obtained using the following process:
+ * <ul>
+ * <li>The persistent state of the table is loaded.
+ * If the tableSessionStoreManager binding has not been bound, the state is loaded 
+ * from a persistent property within the component (it is null at the beginning). 
+ * Otherwise the supplied
+ * {@link  org.apache.tapestry.contrib.table.model.ITableSessionStoreManager} is used
+ * to load the persistent state.
+ * <li>The table model is recreated using the 
+ * {@link org.apache.tapestry.contrib.table.model.ITableSessionStateManager} that
+ * could be supplied using the tableSessionStateManager binding 
+ * (but has a default value and is therefore not required).
+ * <li>If the {@link org.apache.tapestry.contrib.table.model.ITableSessionStateManager}
+ * returns null, then a table model is taken from the tableModel binding. Thus, if
+ * the {@link org.apache.tapestry.contrib.table.model.common.NullTableSessionStateManager}
+ * is used, the table model would be taken from the tableModel binding every time.
+ * </ul>
+ * Just before the rendering phase the persistent state of the model is saved in
+ * the session. This process occurs in reverse:
+ * <ul>
+ * <li>The persistent state of the model is taken via the 
+ * {@link org.apache.tapestry.contrib.table.model.ITableSessionStateManager}.
+ * <li>If the tableSessionStoreManager binding has not been bound, the persistent
+ * state is saved as a persistent page property. Otherwise the supplied
+ * {@link  org.apache.tapestry.contrib.table.model.ITableSessionStoreManager} is used
+ * to save the persistent state. Use of the 
+ * {@link  org.apache.tapestry.contrib.table.model.ITableSessionStoreManager} 
+ * is usually necessary when tables with the same model have to be used across 
+ * multiple pages, and hence the state has to be saved in the Visit, rather than
+ * in a persistent component property.
+ * </ul>
+ * <p>
+ * 
+ * <p> 
+ * Please see the Component Reference for details on how to use this component. 
+ * 
+ *  [<a href="../../../../../../../ComponentReference/contrib.TableView.html">Component Reference</a>]
+ * 
+ * @author mindbridge
+ * @version $Id$
+ */
+public abstract class TableView
+    extends BaseComponent
+    implements PageDetachListener, PageRenderListener, ITableModelSource
+{
+    // Component properties
+    private ITableSessionStateManager m_objDefaultSessionStateManager = null;
+    private ITableColumnModel m_objColumnModel = null;
+
+    // Transient objects
+    private ITableModel m_objTableModel;
+    private ITableModel m_objCachedTableModelValue;
+
+    // enhanced parameter methods
+    public abstract ITableModel getTableModelValue();
+    public abstract Object getSource();
+    public abstract Object getColumns();
+    public abstract IBinding getColumnsBinding();
+    public abstract IBinding getPageSizeBinding();
+    public abstract String getInitialSortColumn();
+    public abstract boolean getInitialSortOrder();
+    public abstract ITableSessionStateManager getTableSessionStateManager();
+    public abstract ITableSessionStoreManager getTableSessionStoreManager();
+    public abstract IComponent getColumnSettingsContainer();
+
+    // enhanced property methods
+    public abstract Serializable getSessionState();
+    public abstract void setSessionState(Serializable sessionState);
+
+    /**
+     *  The component constructor. Invokes the component member initializations. 
+     */
+    public TableView()
+    {
+        initialize();
+    }
+
+    /**
+     *  Invokes the component member initializations.
+     *  
+     *  @see org.apache.tapestry.event.PageDetachListener#pageDetached(PageEvent)
+     */
+    public void pageDetached(PageEvent objEvent)
+    {
+        initialize();
+    }
+
+    /**
+     *  Initialize the component member variables.
+     */
+    private void initialize()
+    {
+        m_objTableModel = null;
+        m_objCachedTableModelValue = null;
+    }
+
+    /**
+     *  Resets the table by removing any stored table state. 
+     *  This means that the current column to sort on and the current page will be
+     *  forgotten and all data will be reloaded.
+     */
+    public void reset()
+    {
+		initialize();
+        storeSessionState(null);
+    }
+
+    public ITableModel getCachedTableModelValue()
+    {
+        if (m_objCachedTableModelValue == null)
+            m_objCachedTableModelValue = getTableModelValue();
+        return m_objCachedTableModelValue;
+    }
+
+    /**
+     *  Returns the tableModel.
+     * 
+     *  @return ITableModel the table model used by the table components
+     */
+    public ITableModel getTableModel()
+    {
+        // if null, first try to recreate the model from the session state
+        if (m_objTableModel == null)
+        {
+            Serializable objState = loadSessionState();
+            m_objTableModel = getTableSessionStateManager().recreateTableModel(objState);
+        }
+
+        // if the session state does not help, get the model from the binding
+        if (m_objTableModel == null)
+            m_objTableModel = getCachedTableModelValue();
+
+        // if the model from the binding is null, build a model from source and columns
+        if (m_objTableModel == null)
+            m_objTableModel = generateTableModel(null);
+
+        if (m_objTableModel == null)
+            throw new ApplicationRuntimeException(
+                TableUtils.format("missing-table-model", getExtendedId()));
+
+        return m_objTableModel;
+    }
+
+    /**
+     *  Generate a table model using the 'source' and 'columns' parameters.
+     * 
+     *  @return the newly generated table model
+     */
+    protected ITableModel generateTableModel(SimpleTableState objState)
+    {
+        // create a new table state if none is passed
+        if (objState == null)
+        {
+            objState = new SimpleTableState();
+            objState.getSortingState().setSortColumn(getInitialSortColumn(), getInitialSortOrder());
+        }
+
+        // update the page size if set in the parameter
+        IBinding objPageSizeBinding = getPageSizeBinding();
+        if (objPageSizeBinding != null)
+            objState.getPagingState().setPageSize(objPageSizeBinding.getInt());
+
+        // get the column model. if not possible, return null.
+        ITableColumnModel objColumnModel = getTableColumnModel();
+        if (objColumnModel == null)
+            return null;
+
+        Object objSourceValue = getSource();
+        if (objSourceValue == null)
+            return null;
+
+        // if the source parameter is of type {@link IBasicTableModel}, 
+        // create and return an appropriate wrapper
+        if (objSourceValue instanceof IBasicTableModel)
+            return new BasicTableModelWrap(
+                (IBasicTableModel) objSourceValue,
+                objColumnModel,
+                objState);
+
+        // otherwise, the source parameter must contain the data to be displayed
+        ITableDataModel objDataModel = null;
+        if (objSourceValue instanceof Object[])
+            objDataModel = new SimpleListTableDataModel((Object[]) objSourceValue);
+        else if (objSourceValue instanceof List)
+            objDataModel = new SimpleListTableDataModel((List) objSourceValue);
+        else if (objSourceValue instanceof Collection)
+            objDataModel = new SimpleListTableDataModel((Collection) objSourceValue);
+        else if (objSourceValue instanceof Iterator)
+            objDataModel = new SimpleListTableDataModel((Iterator) objSourceValue);
+
+        if (objDataModel == null)
+            throw new ApplicationRuntimeException(
+                TableUtils.format(
+                    "invalid-table-source",
+                    getExtendedId(),
+                    objSourceValue.getClass()));
+
+        return new SimpleTableModel(objDataModel, objColumnModel, objState);
+    }
+
+    /**
+     *  Returns the table column model as specified by the 'columns' binding.
+     *  If the value of the 'columns' binding is of a type different than
+     *  ITableColumnModel, this method makes the appropriate conversion. 
+     * 
+     *  @return The table column model as specified by the 'columns' binding
+     */
+    protected ITableColumnModel getTableColumnModel()
+    {
+        Object objColumns = getColumns();
+
+        if (objColumns == null)
+            return null;
+
+        if (objColumns instanceof ITableColumnModel)
+        {
+            return (ITableColumnModel) objColumns;
+        }
+
+        if (objColumns instanceof Iterator)
+        {
+            // convert to List
+            Iterator objColumnsIterator = (Iterator) objColumns;
+            List arrColumnsList = new ArrayList();
+            CollectionUtils.addAll(arrColumnsList, objColumnsIterator);
+            objColumns = arrColumnsList;
+        }
+
+        if (objColumns instanceof List)
+        {
+            // validate that the list contains only ITableColumn instances
+            List arrColumnsList = (List) objColumns;
+            int nColumnsNumber = arrColumnsList.size();
+            for (int i = 0; i < nColumnsNumber; i++)
+            {
+                if (!(arrColumnsList.get(i) instanceof ITableColumn))
+                    throw new ApplicationRuntimeException(
+                        TableUtils.format("columns-only-please", getExtendedId()));
+            }
+            //objColumns = arrColumnsList.toArray(new ITableColumn[nColumnsNumber]);
+            return new SimpleTableColumnModel(arrColumnsList);
+        }
+
+        if (objColumns instanceof ITableColumn[])
+        {
+            return new SimpleTableColumnModel((ITableColumn[]) objColumns);
+        }
+
+        if (objColumns instanceof String)
+        {
+            String strColumns = (String) objColumns;
+            if (getColumnsBinding().isInvariant())
+            {
+                // if the binding is invariant, create the columns only once
+                if (m_objColumnModel == null)
+                    m_objColumnModel = generateTableColumnModel(strColumns);
+                return m_objColumnModel;
+            }
+
+            // if the binding is not invariant, create them every time
+            return generateTableColumnModel(strColumns);
+        }
+
+        throw new ApplicationRuntimeException(
+            TableUtils.format("invalid-table-columns", getExtendedId(), objColumns.getClass()));
+    }
+
+    /**
+     *  Generate a table column model out of the description string provided.
+     *  Entries in the description string are separated by commas.
+     *  Each column entry is of the format name, name:expression, 
+     *  or name:displayName:expression.
+     *  An entry prefixed with ! represents a non-sortable column.
+     *  If the whole description string is prefixed with *, it represents
+     *  columns to be included in a Form. 
+     * 
+     *  @param strDesc the description of the column model to be generated
+     *  @return a table column model based on the provided description
+     */
+    protected ITableColumnModel generateTableColumnModel(String strDesc)
+    {
+        IComponent objColumnSettingsContainer = getColumnSettingsContainer();
+        return TableUtils.generateTableColumnModel(strDesc, this, objColumnSettingsContainer);
+    }
+
+    /**
+     *  The default session state manager to be used in case no such manager
+     *  is provided by the corresponding parameter.
+     * 
+     *  @return the default session state manager
+     */
+    public ITableSessionStateManager getDefaultTableSessionStateManager()
+    {
+        if (m_objDefaultSessionStateManager == null)
+            m_objDefaultSessionStateManager = new TableViewSessionStateManager(this);
+        return m_objDefaultSessionStateManager;
+    }
+
+    /**
+     *  Invoked when there is a modification of the table state and it needs to be saved
+     *  
+     *  @see org.apache.tapestry.contrib.table.model.ITableModelSource#fireObservedStateChange()
+     */
+    public void fireObservedStateChange()
+    {
+        saveSessionState();
+    }
+
+    /**
+     *  Ensures that the table state is saved before the render phase begins 
+     *  in case there are modifications for which {@link #fireObservedStateChange()} 
+     *  has not been invoked.
+     * 
+     * @see org.apache.tapestry.event.PageRenderListener#pageBeginRender(org.apache.tapestry.event.PageEvent)
+     */
+    public void pageBeginRender(PageEvent event)
+    {
+        // 'suspenders': save the table model if it has been already loaded.
+        // this means that if a change has been made explicitly in a listener, 
+        // it will be saved. this is the last place before committing the changes 
+        // where a save can occur  
+        if (m_objTableModel != null)
+            saveSessionState();
+    }
+
+    /**
+     *  @see org.apache.tapestry.event.PageRenderListener#pageEndRender(PageEvent)
+     */
+    public void pageEndRender(PageEvent objEvent)
+    {
+    }
+
+    /**
+     *  Saves the table state using the SessionStateManager to determine 
+     *  what to save and the SessionStoreManager to determine where to save it.  
+     *
+     */
+    protected void saveSessionState()
+    {
+        ITableModel objModel = getTableModel();
+        Serializable objState = getTableSessionStateManager().getSessionState(objModel);
+        storeSessionState(objState);
+    }
+
+    /**
+     *  Loads the table state using the SessionStoreManager.
+     * 
+     *  @return the stored table state
+     */
+    protected Serializable loadSessionState()
+    {
+        ITableSessionStoreManager objManager = getTableSessionStoreManager();
+        if (objManager != null)
+            return objManager.loadState(getPage().getRequestCycle());
+        return getSessionState();
+    }
+
+    /**
+     *  Stores the table state using the SessionStoreManager.
+     * 
+     *  @param objState the table state to store
+     */
+    protected void storeSessionState(Serializable objState)
+    {
+        ITableSessionStoreManager objManager = getTableSessionStoreManager();
+        if (objManager != null)
+            objManager.saveState(getPage().getRequestCycle(), objState);
+        else
+            setSessionState(objState);
+    }
+
+    /**
+     *  Make sure that the values stored in the model are useable and correct.
+     *  The changes made here are not saved.  
+     */
+    protected void validateValues()
+    {
+        ITableModel objModel = getTableModel();
+
+        // make sure current page is within the allowed range
+        ITablePagingState objPagingState = objModel.getPagingState();
+        int nCurrentPage = objPagingState.getCurrentPage();
+        int nPageCount = objModel.getPageCount();
+        if (nCurrentPage >= nPageCount)
+        {
+            // the current page is greater than the page count. adjust.
+            nCurrentPage = nPageCount - 1;
+            objPagingState.setCurrentPage(nCurrentPage);
+        }
+        if (nCurrentPage < 0)
+        {
+            // the current page is before the first page. adjust.
+            nCurrentPage = 0;
+            objPagingState.setCurrentPage(nCurrentPage);
+        }
+    }
+
+    /**
+     *  Stores a pointer to this component in the Request Cycle while rendering
+     *  so that wrapped components have access to it.
+     * 
+     *  @see org.apache.tapestry.BaseComponent#renderComponent(IMarkupWriter, IRequestCycle)
+     */
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        Object objOldValue = cycle.getAttribute(ITableModelSource.TABLE_MODEL_SOURCE_ATTRIBUTE);
+        cycle.setAttribute(ITableModelSource.TABLE_MODEL_SOURCE_ATTRIBUTE, this);
+
+        initialize();
+        validateValues();
+        super.renderComponent(writer, cycle);
+
+        cycle.setAttribute(ITableModelSource.TABLE_MODEL_SOURCE_ATTRIBUTE, objOldValue);
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableView.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableView.jwc
new file mode 100644
index 0000000..b2324fc
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableView.jwc
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!--  $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+    "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+    "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.contrib.table.components.TableView" 
+    allow-body="yes" allow-informal-parameters="yes">
+    
+    <description>
+        The main lower-level Table component.
+        This component should wrap all other lower-level Table components such as 
+        TablePages and TableRows, as it provides the data they use.
+    </description>
+    
+    <parameter name="tableModel" 
+        type="org.apache.tapestry.contrib.table.model.ITableModel" 
+        property-name="tableModelValue"
+        required="no" 
+        direction="auto"
+        default-value="null">
+        <description>
+            The model describing the data to be presented by the table components.
+            This parameter is optional, but either the 'tableModel' or both
+            'source' and 'columns' parameters must be provided.
+        </description>
+    </parameter>
+    
+    <parameter name="source" type="java.lang.Object" required="no" direction="auto" default-value="null">
+        <description>
+            The data to be displayed by the component. This parameter is available as
+            an alternative to tableModel and must be used in combination with the
+            'columns' parameter. 
+            The parameter must be an array of values, a collection, an iterator, 
+            or an object implementing the IBasicTableModel interface.
+        </description>
+    </parameter>
+    
+    <parameter name="columns" type="java.lang.Object" required="no" direction="auto" default-value="null">
+        <description>
+            The table columns to be displayed. 
+            The parameter must be an array, a list, or an Iterator of ITableColumn objects,
+            an ITableColumnModel, or a String describing the columns (see documentation).
+        </description>
+    </parameter>
+    
+	<parameter name="pageSize" 
+		type="int" 
+		required="no">
+        <description>
+            The number of records displayed per page when source/columns are used.
+            The page size is 10 by default.
+        </description>
+    </parameter>
+  
+	<parameter name="initialSortColumn" 
+		type="java.lang.String" 
+		required="no"
+        direction="auto"
+        default-value="null">
+        <description>
+            The id of the column to initially sort the table by.
+            The column is set to null by default, i.e. there is no sorting.
+        </description>
+    </parameter>
+  
+	<parameter name="initialSortOrder" 
+		type="boolean" 
+		required="no"
+        direction="auto"
+        default-value="false">
+        <description>
+            The order of the initial sorting.
+            Set this parameter to 'false' to sort in an ascending order
+            and to 'true' to sort in a descending one.
+        </description>
+    </parameter>
+  
+    <parameter name="tableSessionStateManager" 
+        type="org.apache.tapestry.contrib.table.model.ITableSessionStateManager" 
+        required="no"
+        direction="auto"
+        default-value="defaultTableSessionStateManager">
+        <description>
+            The manager defining what part of the table model will be stored in 
+            the session.
+        </description>
+    </parameter>
+    
+    <parameter name="tableSessionStoreManager" 
+        type="org.apache.tapestry.contrib.table.model.ITableSessionStoreManager" 
+        required="no"
+        direction="auto"
+        default-value="null">
+        <description>
+            The manager defining where the session data will be stored.
+        </description>
+    </parameter>
+    
+    <parameter name="columnSettingsContainer" 
+        type="org.apache.tapestry.IComponent" 
+        required="no" 
+        direction="auto" 
+        default-value="container">
+        <description>
+            The component where Block and messages are pulled from when using source/columns.
+        </description>
+    </parameter>
+    
+    <parameter name="element" type="java.lang.String" required="no" default-value="'table'">
+        <description>
+            The tag with which the component will be inserted in the generated content.
+        </description>
+    </parameter>
+    
+    <property-specification name="sessionState" type="java.io.Serializable" persistent="yes"/>
+    
+    <component id="table" type="Any" inherit-informal-parameters="yes">
+        <inherited-binding name="element" parameter-name="element"/>
+    </component>
+    
+    <component id="insertWrapped" type="RenderBody"/>
+    
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableViewSessionStateManager.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableViewSessionStateManager.java
new file mode 100644
index 0000000..b54ec2c
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/TableViewSessionStateManager.java
@@ -0,0 +1,69 @@
+//  Copyright 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.tapestry.contrib.table.components;
+
+import java.io.Serializable;
+
+import org.apache.tapestry.contrib.table.model.ITableModel;
+import org.apache.tapestry.contrib.table.model.ITableSessionStateManager;
+import org.apache.tapestry.contrib.table.model.simple.SimpleTableState;
+
+/**
+ *  Acts like {@link org.apache.tapestry.contrib.table.model.common.FullTableSessionStateManager} 
+ *  if the model is provided via the tableModel parameter; 
+ *  saves only the model state otherwise. 
+ * 
+ *  @author mindbridge
+ *  @version $Id$
+ */
+public class TableViewSessionStateManager implements ITableSessionStateManager
+{
+    private TableView m_objView;
+
+    public TableViewSessionStateManager(TableView objView)
+    {
+        m_objView = objView;
+    }
+    
+    /**
+     * @see org.apache.tapestry.contrib.table.model.ITableSessionStateManager#getSessionState(org.apache.tapestry.contrib.table.model.ITableModel)
+     */
+    public Serializable getSessionState(ITableModel objModel)
+    {
+        // if the model is provided using the 'tableModel' parameter, 
+        // emulate FullTableSessionStateManager and save everything
+        // (backward compatibility)
+        if (m_objView.getCachedTableModelValue() != null)
+            return (Serializable) objModel;
+            
+        // otherwise save only the state
+        return new SimpleTableState(objModel.getPagingState(), objModel.getSortingState());
+    }
+
+    /**
+     * @see org.apache.tapestry.contrib.table.model.ITableSessionStateManager#recreateTableModel(java.io.Serializable)
+     */
+    public ITableModel recreateTableModel(Serializable objState)
+    {
+        // if the state implements ITableModel, return itself
+        // (backward compatibility)
+        if (objState instanceof ITableModel)
+            return (ITableModel) objState;
+            
+        // otherwise have the component re-generate the model using the provided state
+        return m_objView.generateTableModel((SimpleTableState) objState);
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnComponent.html b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnComponent.html
new file mode 100644
index 0000000..6dcb718
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnComponent.html
@@ -0,0 +1,18 @@
+<!-- $Id$ -->
+
+<span jwcid="$content$">
+
+<span jwcid="condSorted">
+	<table border=0 cellspacing=0 cellpadding=0 align="center">
+	<tr>
+	<td><a jwcid="linkColumn"><span jwcid="insertSortedColumn"/></a></td>
+	<span jwcid="condSort"><td>&nbsp;<span jwcid="imageSort" align="center"/></td></span>
+	</tr>
+	</table>
+</span>
+
+<span jwcid="condNotSorted">
+	<span jwcid="insertNotSortedColumn"/>
+</span>
+
+</span>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnComponent.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnComponent.java
new file mode 100644
index 0000000..9b83796
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnComponent.java
@@ -0,0 +1,164 @@
+//  Copyright 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.tapestry.contrib.table.components.inserted;
+
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.IAsset;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.table.components.TableColumns;
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+import org.apache.tapestry.contrib.table.model.ITableModel;
+import org.apache.tapestry.contrib.table.model.ITableModelSource;
+import org.apache.tapestry.contrib.table.model.ITableRendererListener;
+import org.apache.tapestry.contrib.table.model.ITableSortingState;
+import org.apache.tapestry.contrib.table.model.simple.SimpleTableColumn;
+import org.apache.tapestry.event.PageDetachListener;
+import org.apache.tapestry.event.PageEvent;
+import org.apache.tapestry.util.ComponentAddress;
+
+/**
+ * A component that renders the default column header.
+ * 
+ * If the current column is sortable, it renders the header as a link.
+ * Clicking on the link causes the table to be sorted on that column.
+ * Clicking on the link again causes the sorting order to be reversed.
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public class SimpleTableColumnComponent
+	extends BaseComponent
+	implements ITableRendererListener, PageDetachListener
+{
+	// transient
+	private ITableColumn m_objColumn;
+	private ITableModelSource m_objModelSource;
+
+	public SimpleTableColumnComponent()
+	{
+		init();
+	}
+
+	/**
+	 * @see org.apache.tapestry.event.PageDetachListener#pageDetached(PageEvent)
+	 */
+	public void pageDetached(PageEvent arg0)
+	{
+		init();
+	}
+
+	private void init()
+	{
+		m_objColumn = null;
+		m_objModelSource = null;
+	}
+
+
+    /**
+     * @see org.apache.tapestry.contrib.table.model.ITableRendererListener#initializeRenderer(IRequestCycle, ITableModelSource, ITableColumn, Object)
+     */
+    public void initializeRenderer(
+        IRequestCycle objCycle,
+        ITableModelSource objSource,
+        ITableColumn objColumn,
+        Object objRow)
+    {
+        m_objModelSource = objSource;
+        m_objColumn = objColumn;
+    }
+
+	public ITableModel getTableModel()
+	{
+		return m_objModelSource.getTableModel();
+	}
+
+	public boolean getColumnSorted()
+	{
+		return m_objColumn.getSortable();
+	}
+
+	public String getDisplayName()
+	{
+        if (m_objColumn instanceof SimpleTableColumn) {
+            SimpleTableColumn objSimpleColumn = (SimpleTableColumn) m_objColumn;
+    		return objSimpleColumn.getDisplayName();
+        }
+        return m_objColumn.getColumnName();
+	}
+
+	public boolean getIsSorted()
+	{
+		ITableSortingState objSortingState = getTableModel().getSortingState();
+		String strSortColumn = objSortingState.getSortColumn();
+		return m_objColumn.getColumnName().equals(strSortColumn);
+	}
+
+	public IAsset getSortImage()
+	{
+		IAsset objImageAsset;
+
+		IRequestCycle objCycle = getPage().getRequestCycle();
+		ITableSortingState objSortingState = getTableModel().getSortingState();
+		if (objSortingState.getSortOrder()
+			== ITableSortingState.SORT_ASCENDING)
+		{
+			objImageAsset =
+				(IAsset) objCycle.getAttribute(
+					TableColumns.TABLE_COLUMN_ARROW_UP_ATTRIBUTE);
+			if (objImageAsset == null)
+				objImageAsset = getAsset("sortUp");
+		}
+		else
+		{
+			objImageAsset =
+				(IAsset) objCycle.getAttribute(
+					TableColumns.TABLE_COLUMN_ARROW_DOWN_ATTRIBUTE);
+			if (objImageAsset == null)
+				objImageAsset = getAsset("sortDown");
+		}
+
+		return objImageAsset;
+	}
+
+	public Object[] getColumnSelectedParameters()
+	{
+		return new Object[] {
+			new ComponentAddress(m_objModelSource),
+			m_objColumn.getColumnName()};
+	}
+
+	public void columnSelected(IRequestCycle objCycle)
+	{
+		Object[] arrArgs = objCycle.getServiceParameters();
+		ComponentAddress objAddr = (ComponentAddress) arrArgs[0];
+		String strColumnName = (String) arrArgs[1];
+
+		ITableModelSource objSource =
+			(ITableModelSource) objAddr.findComponent(objCycle);
+		ITableModel objModel = objSource.getTableModel();
+
+		ITableSortingState objState = objModel.getSortingState();
+		if (strColumnName.equals(objState.getSortColumn()))
+			objState.setSortColumn(strColumnName, !objState.getSortOrder());
+		else
+			objState.setSortColumn(
+				strColumnName,
+				ITableSortingState.SORT_ASCENDING);
+
+		// ensure that the change is saved
+		objSource.fireObservedStateChange();
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnComponent.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnComponent.jwc
new file mode 100644
index 0000000..93065bc
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnComponent.jwc
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!--  $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.contrib.table.components.inserted.SimpleTableColumnComponent" 
+	allow-informal-parameters="yes">
+	
+	<description>
+	</description>
+	
+	<component id="condSorted" type="Conditional">
+		<binding name="condition" expression="columnSorted"/>
+	</component>
+	
+	<component id="condNotSorted" type="Conditional">
+		<binding name="condition" expression="columnSorted"/>
+		<static-binding name="invert">true</static-binding>
+	</component>
+	
+	<component id="insertSortedColumn" type="Insert">
+		<binding name="value" expression="displayName"/>
+	</component>
+	
+	<component id="insertNotSortedColumn" type="Insert">
+		<binding name="value" expression="displayName"/>
+	</component>
+	
+	<component id="linkColumn" type="DirectLink">
+		<binding name="listener" expression="listeners.columnSelected"/>
+		<binding name="parameters" expression="columnSelectedParameters"/>
+	</component>
+	
+	<component id="imageSort" type="Image">
+		<binding name="image" expression="sortImage"/>
+	</component>
+	
+	<component id="condSort" type="Conditional">
+		<binding name="condition" expression="isSorted"/>
+	</component>
+	
+    <private-asset name="sortDown" resource-path="arrow-down.gif"/>
+    <private-asset name="sortUp" resource-path="arrow-up.gif"/>
+</component-specification>
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnFormComponent.html b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnFormComponent.html
new file mode 100644
index 0000000..6dcb718
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnFormComponent.html
@@ -0,0 +1,18 @@
+<!-- $Id$ -->
+
+<span jwcid="$content$">
+
+<span jwcid="condSorted">
+	<table border=0 cellspacing=0 cellpadding=0 align="center">
+	<tr>
+	<td><a jwcid="linkColumn"><span jwcid="insertSortedColumn"/></a></td>
+	<span jwcid="condSort"><td>&nbsp;<span jwcid="imageSort" align="center"/></td></span>
+	</tr>
+	</table>
+</span>
+
+<span jwcid="condNotSorted">
+	<span jwcid="insertNotSortedColumn"/>
+</span>
+
+</span>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnFormComponent.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnFormComponent.java
new file mode 100644
index 0000000..bfd9795
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnFormComponent.java
@@ -0,0 +1,137 @@
+//  Copyright 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.tapestry.contrib.table.components.inserted;
+
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.IAsset;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.table.components.TableColumns;
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+import org.apache.tapestry.contrib.table.model.ITableModel;
+import org.apache.tapestry.contrib.table.model.ITableModelSource;
+import org.apache.tapestry.contrib.table.model.ITableRendererListener;
+import org.apache.tapestry.contrib.table.model.ITableSortingState;
+import org.apache.tapestry.contrib.table.model.simple.SimpleTableColumn;
+
+/**
+ * A component that renders the default column header in a form.
+ * 
+ * If the current column is sortable, it renders the header as a link.
+ * Clicking on the link causes the table to be sorted on that column.
+ * Clicking on the link again causes the sorting order to be reversed.
+ * 
+ * This component renders links that cause the form to be submitted. 
+ * This ensures that the updated data in the other form fields is preserved. 
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public abstract class SimpleTableColumnFormComponent
+	extends BaseComponent
+	implements ITableRendererListener
+{
+
+    public abstract ITableColumn getTableColumn();
+    public abstract void setTableColumn(ITableColumn objColumn);
+
+    public abstract ITableModelSource getTableModelSource();
+    public abstract void setTableModelSource(ITableModelSource objSource);
+
+    public abstract String getSelectedColumnName();
+
+    /**
+     * @see org.apache.tapestry.contrib.table.model.ITableRendererListener#initializeRenderer(IRequestCycle, ITableModelSource, ITableColumn, Object)
+     */
+    public void initializeRenderer(
+        IRequestCycle objCycle,
+        ITableModelSource objSource,
+        ITableColumn objColumn,
+        Object objRow)
+    {
+        setTableModelSource(objSource);
+        setTableColumn(objColumn);
+    }
+
+	public ITableModel getTableModel()
+	{
+		return getTableModelSource().getTableModel();
+	}
+
+	public boolean getColumnSorted()
+	{
+		return getTableColumn().getSortable();
+	}
+
+	public String getDisplayName()
+	{
+        ITableColumn objColumn = getTableColumn();
+        
+        if (objColumn instanceof SimpleTableColumn) {
+            SimpleTableColumn objSimpleColumn = (SimpleTableColumn) objColumn;
+    		return objSimpleColumn.getDisplayName();
+        }
+        return objColumn.getColumnName();
+	}
+
+	public boolean getIsSorted()
+	{
+		ITableSortingState objSortingState = getTableModel().getSortingState();
+		String strSortColumn = objSortingState.getSortColumn();
+		return getTableColumn().getColumnName().equals(strSortColumn);
+	}
+
+	public IAsset getSortImage()
+	{
+		IAsset objImageAsset;
+
+		IRequestCycle objCycle = getPage().getRequestCycle();
+		ITableSortingState objSortingState = getTableModel().getSortingState();
+		if (objSortingState.getSortOrder()
+			== ITableSortingState.SORT_ASCENDING)
+		{
+			objImageAsset =
+				(IAsset) objCycle.getAttribute(
+					TableColumns.TABLE_COLUMN_ARROW_UP_ATTRIBUTE);
+			if (objImageAsset == null)
+				objImageAsset = getAsset("sortUp");
+		}
+		else
+		{
+			objImageAsset =
+				(IAsset) objCycle.getAttribute(
+					TableColumns.TABLE_COLUMN_ARROW_DOWN_ATTRIBUTE);
+			if (objImageAsset == null)
+				objImageAsset = getAsset("sortDown");
+		}
+
+		return objImageAsset;
+	}
+
+	public void columnSelected(IRequestCycle objCycle)
+	{
+        String strColumnName = getSelectedColumnName();
+		ITableSortingState objState = getTableModel().getSortingState();
+		if (strColumnName.equals(objState.getSortColumn()))
+			objState.setSortColumn(strColumnName, !objState.getSortOrder());
+		else
+			objState.setSortColumn(
+				strColumnName,
+				ITableSortingState.SORT_ASCENDING);
+
+		// ensure that the change is saved
+		getTableModelSource().fireObservedStateChange();
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnFormComponent.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnFormComponent.jwc
new file mode 100644
index 0000000..7f36541
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnFormComponent.jwc
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!--  $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.contrib.table.components.inserted.SimpleTableColumnFormComponent" 
+	allow-informal-parameters="yes">
+	
+	<description>
+	</description>
+	
+	<property-specification name="tableModelSource" 
+		type="org.apache.tapestry.contrib.table.model.ITableModelSource"
+		initial-value="null"/>
+	
+	<property-specification name="tableColumn" 
+		type="org.apache.tapestry.contrib.table.model.ITableColumn"
+		initial-value="null"/>
+	
+	<property-specification name="selectedColumnName" type="java.lang.String"/>
+	
+	<component id="condSorted" type="FormConditional">
+		<binding name="condition" expression="columnSorted"/>
+	</component>
+	
+	<component id="condNotSorted" type="FormConditional">
+		<binding name="condition" expression="!columnSorted"/>
+	</component>
+	
+	<component id="insertSortedColumn" type="Insert">
+		<binding name="value" expression="displayName"/>
+	</component>
+	
+	<component id="insertNotSortedColumn" type="Insert">
+		<binding name="value" expression="displayName"/>
+	</component>
+	
+	<component id="linkColumn" type="LinkSubmit">
+		<binding name="listener" expression="listeners.columnSelected"/>
+		<binding name="tag" expression="tableColumn.columnName"/>
+		<binding name="selected" expression="selectedColumnName"/>
+	</component>
+	
+	<component id="imageSort" type="Image">
+		<binding name="image" expression="sortImage"/>
+	</component>
+	
+	<component id="condSort" type="FormConditional">
+		<binding name="condition" expression="isSorted"/>
+	</component>
+	
+    <private-asset name="sortDown" resource-path="arrow-down.gif"/>
+    <private-asset name="sortUp" resource-path="arrow-up.gif"/>
+    
+</component-specification>
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnPage.html b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnPage.html
new file mode 100644
index 0000000..a1d70de
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnPage.html
@@ -0,0 +1,8 @@
+<!-- $Id$ -->
+
+<span jwcid="$content$">
+
+	<span jwcid="tableColumnComponent"/>
+	<span jwcid="tableColumnFormComponent"/>
+
+</span>
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnPage.page b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnPage.page
new file mode 100644
index 0000000..b84772d
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/SimpleTableColumnPage.page
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE page-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<page-specification>
+
+	<component id="tableColumnComponent" type="SimpleTableColumnComponent"/>
+	<component id="tableColumnFormComponent" type="SimpleTableColumnFormComponent"/>
+
+</page-specification>
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/arrow-down.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/arrow-down.gif
new file mode 100644
index 0000000..d9339a6
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/arrow-down.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/arrow-up.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/arrow-up.gif
new file mode 100644
index 0000000..b70a479
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/components/inserted/arrow-up.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/CTableDataModelEvent.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/CTableDataModelEvent.java
new file mode 100644
index 0000000..c402687
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/CTableDataModelEvent.java
@@ -0,0 +1,25 @@
+//  Copyright 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.tapestry.contrib.table.model;
+
+/**
+ * @author mindbridge
+ *
+ */
+public class CTableDataModelEvent
+{
+    public CTableDataModelEvent() {
+    }
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/IBasicTableModel.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/IBasicTableModel.java
new file mode 100644
index 0000000..aa1ac30
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/IBasicTableModel.java
@@ -0,0 +1,43 @@
+//  Copyright 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.tapestry.contrib.table.model;
+
+import java.util.Iterator;
+
+/**
+ * A simplified version of the table model that concerns itself only with
+ * providing the data on the current page. 
+ * 
+ * @version $Id$
+ * @author mindbridge
+ * @since 3.0
+ */
+public interface IBasicTableModel
+{
+    /**
+     *  Returns the number of all records
+     *  @return the number of all rows
+     **/
+    int getRowCount();
+
+    /** 
+     *  Returns the rows on the current page.
+     *  @param nFirst the index of the first item to be dispayed
+     *  @param nPageSize the number of items to be displayed
+     *  @param objSortColumn the column to sort by or null if there is no sorting
+     *  @param bSortOrder determines the sorting order (ascending or descending)
+     **/
+    Iterator getCurrentPageRows(int nFirst, int nPageSize, ITableColumn objSortColumn, boolean bSortOrder);
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/IPrimaryKeyConvertor.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/IPrimaryKeyConvertor.java
new file mode 100644
index 0000000..ec66178
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/IPrimaryKeyConvertor.java
@@ -0,0 +1,43 @@
+//  Copyright 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.tapestry.contrib.table.model;
+
+/**
+ * An interface for converting an object to its primary key and back. 
+ * Typically used to determine how to store a given object as a hidden 
+ * value when rendering a form.
+ * 
+ * @version $Id$
+ * @author mb
+ * @since 3.0
+ */
+public interface IPrimaryKeyConvertor
+{
+    /**
+     * Gets the serializable primary key of the given value
+     * 
+     * @param objValue the value for which a primary key needs to be extracted
+     * @return the serializable primary key of the value
+     */
+    Object getPrimaryKey(Object objValue);
+    
+    /**
+     * Gets the value corresponding the given primary key 
+     *  
+     * @param objPrimaryKey the primary key for which a value needs to be generated
+     * @return the generated value corresponding to the given primary key
+     */
+    Object getValue(Object objPrimaryKey); 
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableColumn.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableColumn.java
new file mode 100644
index 0000000..5ce6c13
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableColumn.java
@@ -0,0 +1,89 @@
+//  Copyright 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.tapestry.contrib.table.model;
+
+import java.util.Comparator;
+
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ * The interface defining a table column. 
+ * 
+ * A column is responsible for presenting a particular part of the data
+ * from the objects in the table. This is done via the getValueRender() method.
+ * 
+ * A column may be sortable, in which case it defines the way in which the
+ * objects in the table must be sorted by providing a Comparator.
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public interface ITableColumn
+{
+	/**
+	 * Method getColumnName provides the name of the column. 
+	 *
+	 * The column name must be unique and is generally used for the identification 
+	 * of the column. It does not have to be the same as the display name 
+	 * via which the column is identified to the user (see the getColumnRender() method).
+	 * @return String the name of the column
+	 */
+	String getColumnName();
+
+	/**
+	 * Method getSortable declares whether the column allows sorting.
+	 * If the column allows sorting, it must also return a valid Comparator
+	 * via the getComparator() method.
+	 * @return boolean whether the column is sortable or not
+	 */
+	boolean getSortable();
+
+	/**
+	 * Method getComparator returns the Comparator to be used to sort 
+	 * the data in the table according to this column. The Comparator must
+	 * accept two different rows, compare them according to this column, 
+	 * and return the appropriate value.
+	 * @return Comparator the Comparator used to sort the table data
+	 */
+	Comparator getComparator();
+
+	/**
+	 * Method getColumnRenderer provides a renderer that takes care of rendering 
+	 * the column in the table header. If the column is sortable, the renderer
+	 * may provide a mechanism to sort the table in an ascending or descending 
+	 * manner.
+	 * @param objCycle the current request cycle
+	 * @param objSource a component that can provide the table model (typically TableView)
+	 * @return IRender the renderer to present the column header
+	 */
+	IRender getColumnRenderer(
+		IRequestCycle objCycle,
+		ITableModelSource objSource);
+
+	/**
+	 * Method getValueRenderer provides a renderer for presenting the value of a 
+	 * particular row in the current column.
+	 * 
+	 * @param objCycle the current request cycle
+	 * @param objSource a component that can provide the table model (typically TableView)
+	 * @param objRow the row data
+	 * @return IRender the renderer to present the value of the row in this column
+	 */
+	IRender getValueRenderer(
+		IRequestCycle objCycle,
+		ITableModelSource objSource,
+		Object objRow);
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableColumnModel.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableColumnModel.java
new file mode 100644
index 0000000..26de305
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableColumnModel.java
@@ -0,0 +1,45 @@
+//  Copyright 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.tapestry.contrib.table.model;
+
+import java.util.Iterator;
+
+/**
+ * Defines a list model of ITableColumn objects
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public interface ITableColumnModel
+{
+	/**
+	 * Method getColumnCount.
+	 * @return int the number of columns in the model
+	 */
+	int getColumnCount();
+
+	/**
+	 * Method getColumn.
+	 * @param strName the name of the requested column
+	 * @return ITableColumn the column with the given name. null if no such column exists.
+	 */
+	ITableColumn getColumn(String strName);
+
+	/**
+	 * Method getColumns.
+	 * @return Iterator an iterator of all columns in the model
+	 */
+	Iterator getColumns();
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableDataModel.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableDataModel.java
new file mode 100644
index 0000000..9cfb723
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableDataModel.java
@@ -0,0 +1,55 @@
+//  Copyright 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.tapestry.contrib.table.model;
+
+import java.util.Iterator;
+
+/**
+ * A model of the table's data
+ * This model need not be used. Implementations may choose to
+ * access data via an abstraction.
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public interface ITableDataModel
+{
+	/**
+	 * Method getRowCount.
+	 * @return int the number of rows in the model
+	 */
+	int getRowCount();
+
+	/**
+	 * Iterates over all of the rows in the model
+	 * @return Iterator the iterator for access to the data
+	 */
+	Iterator getRows();
+    
+	/**
+	 * Method addTableDataModelListener
+     * Adds a listener that is notified when the data in the model is changed
+	 * @param objListener the listener to add
+	 */
+    void addTableDataModelListener(ITableDataModelListener objListener);
+
+	/**
+	 * Method removeTableDataModelListener.
+     * Removes a listener that is notified when the data in the model is changed
+	 * @param objListener the listener to remove
+	 */
+    void removeTableDataModelListener(ITableDataModelListener objListener);
+    
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableDataModelListener.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableDataModelListener.java
new file mode 100644
index 0000000..55026d0
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableDataModelListener.java
@@ -0,0 +1,24 @@
+//  Copyright 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.tapestry.contrib.table.model;
+
+/**
+ * @author mindbridge
+ *
+ */
+public interface ITableDataModelListener
+{
+    void tableDataChanged(CTableDataModelEvent objEvent);
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableModel.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableModel.java
new file mode 100644
index 0000000..a244ca7
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableModel.java
@@ -0,0 +1,54 @@
+//  Copyright 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.tapestry.contrib.table.model;
+
+import java.util.Iterator;
+
+/**
+ * The main interface defining the abstraction containing the table data and state
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public interface ITableModel
+{
+	/**
+	 * Method getColumnModel.
+	 * @return ITableColumnModel the column model of the table
+	 */
+	ITableColumnModel getColumnModel();
+
+	/**
+	 * Method getSortingState.
+	 * @return ITableSortingState the sorting state of the table
+	 */
+	ITableSortingState getSortingState();
+	/**
+	 * Method getPagingState.
+	 * @return ITablePagingState the paging state of the table
+	 */
+	ITablePagingState getPagingState();
+
+	/**
+	 * Method getPageCount.
+	 * @return int the number of pages this table would have given the current data and paging state
+	 */
+	int getPageCount();
+	/**
+	 * Method getCurrentPageRows.
+	 * @return Iterator the rows in the current table page given the current data, sorting, and paging state
+	 */
+	Iterator getCurrentPageRows();
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableModelSource.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableModelSource.java
new file mode 100644
index 0000000..4b8ce2f
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableModelSource.java
@@ -0,0 +1,46 @@
+//  Copyright 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.tapestry.contrib.table.model;
+
+import org.apache.tapestry.IComponent;
+
+/**
+ * A Tapestry component that provides the current table model.
+ * This interface is used for obtaining the table model source by
+ * components wrapped by it, as well as by external renderers,
+ * such as those provided by the column implementations
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public interface ITableModelSource extends IComponent
+{
+    final static String TABLE_MODEL_SOURCE_ATTRIBUTE = "org.apache.tapestry.contrib.table.model.ITableModelSource";
+
+	/**
+	 * Returns the table model currently used
+	 * @return ITableModel the current table model
+	 */
+	ITableModel getTableModel();
+
+	/**
+	 * Notifies the model source that the model state has changed, and 
+     * that it should consider saving it.<p>
+     * This method was added to allow using the table within a Block when 
+     * the pageBeginRender() listener of the implementation will not be called
+     * and automatic state storage will therefore be hard to implement.
+	 */
+    void fireObservedStateChange();
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITablePagingState.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITablePagingState.java
new file mode 100644
index 0000000..ecbd8af
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITablePagingState.java
@@ -0,0 +1,50 @@
+//  Copyright 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.tapestry.contrib.table.model;
+
+/**
+ * An interface defining the management of the table's paging state.
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public interface ITablePagingState
+{
+	/**
+	 * Method getPageSize provides the size of a page in a number of records.
+	 * This value may be meaningless if the model uses a different method for pagination.
+	 * @return int the current page size
+	 */
+	int getPageSize();
+
+	/**
+	 * Method setPageSize updates the size of a page in a number of records.
+	 * This value may be meaningless if the model uses a different method for pagination.
+	 * @param nPageSize the new page size
+	 */
+	void setPageSize(int nPageSize);
+
+	/**
+	 * Gets the currently selected page. The page number is counted from 0.
+	 * @return int the current active page
+	 */
+	int getCurrentPage();
+
+	/**
+	 * Sets the newly selected page. The page number is counted from 0.
+	 * @param nPage the new active page
+	 */
+	void setCurrentPage(int nPage);
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableRendererListener.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableRendererListener.java
new file mode 100644
index 0000000..a24c1a1a
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableRendererListener.java
@@ -0,0 +1,34 @@
+//  Copyright 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.tapestry.contrib.table.model;
+
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ * 
+ * @see org.apache.tapestry.contrib.table.model.common.AbstractTableColumn
+ * @version $Id$
+ * @author mindbridge
+ * @since 2.3
+ */
+public interface ITableRendererListener extends IComponent
+{
+	void initializeRenderer(
+		IRequestCycle objCycle,
+		ITableModelSource objSource,
+		ITableColumn objColumn,
+		Object objRow);
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableRendererSource.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableRendererSource.java
new file mode 100644
index 0000000..c6348f8
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableRendererSource.java
@@ -0,0 +1,47 @@
+//  Copyright 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.tapestry.contrib.table.model;
+
+import java.io.Serializable;
+
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ * This interface provides a renderer to present the data in a table column.
+ * It is usually used by the {@link org.apache.tapestry.contrib.table.model.ITableColumn} 
+ * implementations via aggregation.
+ * 
+ * @see org.apache.tapestry.contrib.table.model.common.AbstractTableColumn
+ * @version $Id$
+ * @author mindbridge
+ * @since 2.3
+ */
+public interface ITableRendererSource extends Serializable
+{
+	/**
+	 * Returns a renderer to present the data of the row in the given column. <p>
+	 * This method can also be used to return a renderer to present the
+	 * heading of the column. In such a case the row passed would be null.
+	 * 
+	 * @see org.apache.tapestry.contrib.table.model.ITableColumn#getValueRenderer(IRequestCycle, ITableModelSource, Object)
+	 */
+	public IRender getRenderer(
+		IRequestCycle objCycle,
+		ITableModelSource objSource,
+		ITableColumn objColumn,
+		Object objRow);
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableRowSource.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableRowSource.java
new file mode 100644
index 0000000..e358297
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableRowSource.java
@@ -0,0 +1,34 @@
+//  Copyright 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.tapestry.contrib.table.model;
+
+/**
+ * A Tapestry component that provides the current row value.
+ * This interface is used for obtaining the row source by components 
+ * wrapped by the row source
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public interface ITableRowSource
+{
+    final static String TABLE_ROW_SOURCE_ATTRIBUTE = "org.apache.tapestry.contrib.table.model.ITableRowSource";
+
+	/**
+	 * Method getTableRow
+	 * @return Object the current table row object
+	 */
+    Object getTableRow();
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableSessionStateManager.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableSessionStateManager.java
new file mode 100644
index 0000000..2358beb
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableSessionStateManager.java
@@ -0,0 +1,45 @@
+//  Copyright 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.tapestry.contrib.table.model;
+
+import java.io.Serializable;
+
+/**
+ * An  interface responsible for determining <b>what</b> data would be stored 
+ * in the session between requests. 
+ * It could be only the table state, it could be entire table including the data,
+ * or it could be nothing at all. 
+ * It is all determined by the implemention of this interface.
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public interface ITableSessionStateManager
+{
+
+	/**
+	 * Method getSessionState extracts the "persistent" portion of the table model
+	 * @param objModel the table model to extract the session state from
+	 * @return Object the session state to be saved between the requests
+	 */
+	Serializable getSessionState(ITableModel objModel);
+
+	/**
+	 * Method recreateTableModel recreates a table model from the saved session state
+	 * @param objState the saved session state
+	 * @return ITableModel the recreated table model
+	 */
+	ITableModel recreateTableModel(Serializable objState);
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableSessionStoreManager.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableSessionStoreManager.java
new file mode 100644
index 0000000..6fb7261
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableSessionStoreManager.java
@@ -0,0 +1,42 @@
+//  Copyright 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.tapestry.contrib.table.model;
+
+import java.io.Serializable;
+
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ * An interface responsible for determining <b>where</b> the session state 
+ * will be saved between requests.
+ *  
+ * @version $Id$
+ * @author mindbridge
+ */
+public interface ITableSessionStoreManager
+{
+	/**
+	 * Method saveState saves the session sate
+	 * @param objCycle the current request cycle
+	 * @param objState the session state to be saved
+	 */
+	void saveState(IRequestCycle objCycle, Serializable objState);
+	/**
+	 * Method loadState loads the session state
+	 * @param objCycle the current request cycle
+	 * @return Object the loaded sessions state
+	 */
+	Serializable loadState(IRequestCycle objCycle);
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableSortingState.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableSortingState.java
new file mode 100644
index 0000000..2fb8002
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ITableSortingState.java
@@ -0,0 +1,46 @@
+//  Copyright 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.tapestry.contrib.table.model;
+
+/**
+ * An interface defining the management of the table's sorting state.
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public interface ITableSortingState
+{
+	static final boolean SORT_ASCENDING = false;
+	static final boolean SORT_DESCENDING = true;
+
+	/**
+	 * Method getSortColumn defines the column that the table should be sorted upon
+	 * @return String the name of the sorting column or null if the table is not sorted
+	 */
+	String getSortColumn();
+
+	/**
+	 * Method getSortOrder defines the direction of the table sorting 
+	 * @return boolean the sorting order (see constants)
+	 */
+	boolean getSortOrder();
+
+	/**
+	 * Method setSortColumn updates the table sorting column and order
+	 * @param strName the name of the column to sort by
+	 * @param bOrder the sorting order (see constants)
+	 */
+	void setSortColumn(String strName, boolean bOrder);
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/AbstractTableColumn.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/AbstractTableColumn.java
new file mode 100644
index 0000000..5d69c1d
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/AbstractTableColumn.java
@@ -0,0 +1,232 @@
+//  Copyright 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.tapestry.contrib.table.model.common;
+
+import java.io.Serializable;
+import java.util.Comparator;
+
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.components.Block;
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+import org.apache.tapestry.contrib.table.model.ITableModelSource;
+import org.apache.tapestry.contrib.table.model.ITableRendererSource;
+import org.apache.tapestry.valid.RenderString;
+
+/**
+ * A base implementation of {@link org.apache.tapestry.contrib.table.model.ITableColumn}
+ * that allows renderers to be set via aggregation.
+ * 
+ * @see org.apache.tapestry.contrib.table.model.ITableRendererSource
+ * @version $Id$
+ * @author mindbridge
+ * @since 2.3
+ */
+public class AbstractTableColumn implements ITableColumn, Serializable
+{
+    /**
+     *  The suffix of the name of the Block that will be used as the column renderer
+     *  for this column 
+     */
+    public final static String COLUMN_RENDERER_BLOCK_SUFFIX = "ColumnHeader";
+
+    /**
+     *  The suffix of the name of the Block that will be used as the value renderer 
+     *  for this column 
+     */
+    public final static String VALUE_RENDERER_BLOCK_SUFFIX = "ColumnValue";
+    
+	private String m_strColumnName;
+	private boolean m_bSortable;
+	private Comparator m_objComparator;
+
+	private ITableRendererSource m_objColumnRendererSource;
+	private ITableRendererSource m_objValueRendererSource;
+
+	public AbstractTableColumn()
+	{
+		this("", false, null);
+	}
+
+	public AbstractTableColumn(
+		String strColumnName,
+		boolean bSortable,
+		Comparator objComparator)
+	{
+		this(strColumnName, bSortable, objComparator, null, null);
+	}
+
+	public AbstractTableColumn(
+		String strColumnName,
+		boolean bSortable,
+		Comparator objComparator,
+		ITableRendererSource objColumnRendererSource,
+		ITableRendererSource objValueRendererSource)
+	{
+		setColumnName(strColumnName);
+		setSortable(bSortable);
+		setComparator(objComparator);
+		setColumnRendererSource(objColumnRendererSource);
+		setValueRendererSource(objValueRendererSource);
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableColumn#getColumnName()
+	 */
+	public String getColumnName()
+	{
+		return m_strColumnName;
+	}
+
+	/**
+	 * Sets the columnName.
+	 * @param columnName The columnName to set
+	 */
+	public void setColumnName(String columnName)
+	{
+		m_strColumnName = columnName;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableColumn#getSortable()
+	 */
+	public boolean getSortable()
+	{
+		return m_bSortable;
+	}
+
+	/**
+	 * Sets whether the column is sortable.
+	 * @param sortable The sortable flag to set
+	 */
+	public void setSortable(boolean sortable)
+	{
+		m_bSortable = sortable;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableColumn#getComparator()
+	 */
+	public Comparator getComparator()
+	{
+		return m_objComparator;
+	}
+
+	/**
+	 * Sets the comparator.
+	 * @param comparator The comparator to set
+	 */
+	public void setComparator(Comparator comparator)
+	{
+		m_objComparator = comparator;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableColumn#getColumnRenderer(IRequestCycle, ITableModelSource)
+	 */
+	public IRender getColumnRenderer(
+		IRequestCycle objCycle,
+		ITableModelSource objSource)
+	{
+		ITableRendererSource objRendererSource =
+			getColumnRendererSource();
+		if (objRendererSource == null)
+		{
+			// log error
+			return new RenderString("");
+		}
+
+		return objRendererSource.getRenderer(objCycle, objSource, this, null);
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableColumn#getValueRenderer(IRequestCycle, ITableModelSource, Object)
+	 */
+	public IRender getValueRenderer(
+		IRequestCycle objCycle,
+		ITableModelSource objSource,
+		Object objRow)
+	{
+		ITableRendererSource objRendererSource = getValueRendererSource();
+		if (objRendererSource == null)
+		{
+			// log error
+			return new RenderString("");
+		}
+
+		return objRendererSource.getRenderer(
+			objCycle,
+			objSource,
+			this,
+			objRow);
+	}
+
+	/**
+	 * Returns the columnRendererSource.
+	 * @return ITableColumnRendererSource
+	 */
+	public ITableRendererSource getColumnRendererSource()
+	{
+		return m_objColumnRendererSource;
+	}
+
+	/**
+	 * Sets the columnRendererSource.
+	 * @param columnRendererSource The columnRendererSource to set
+	 */
+	public void setColumnRendererSource(ITableRendererSource columnRendererSource)
+	{
+		m_objColumnRendererSource = columnRendererSource;
+	}
+
+	/**
+	 * Returns the valueRendererSource.
+     * 
+	 * @return the valueRendererSource of this column
+	 */
+	public ITableRendererSource getValueRendererSource()
+	{
+		return m_objValueRendererSource;
+	}
+
+	/**
+	 * Sets the valueRendererSource.
+     * 
+	 * @param valueRendererSource The valueRendererSource to set
+	 */
+	public void setValueRendererSource(ITableRendererSource valueRendererSource)
+	{
+		m_objValueRendererSource = valueRendererSource;
+	}
+
+    /**
+     *  Use the column name to get the column and value renderer sources 
+     *  from the provided component.
+     *   
+     *  @param objSettingsContainer the component from which to get the settings 
+     */
+    public void loadSettings(IComponent objSettingsContainer)
+    {
+        IComponent objColumnRendererSource = (IComponent) objSettingsContainer.getComponents().get(getColumnName() + COLUMN_RENDERER_BLOCK_SUFFIX);
+        if (objColumnRendererSource != null && objColumnRendererSource instanceof Block)
+            setColumnRendererSource(new BlockTableRendererSource((Block) objColumnRendererSource));
+
+        IComponent objValueRendererSource = (IComponent) objSettingsContainer.getComponents().get(getColumnName() + VALUE_RENDERER_BLOCK_SUFFIX);
+        if (objValueRendererSource != null && objValueRendererSource instanceof Block)
+            setValueRendererSource(new BlockTableRendererSource((Block) objValueRendererSource));
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/AbstractTableDataModel.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/AbstractTableDataModel.java
new file mode 100644
index 0000000..23e5477
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/AbstractTableDataModel.java
@@ -0,0 +1,105 @@
+//  Copyright 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.tapestry.contrib.table.model.common;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.tapestry.contrib.table.model.CTableDataModelEvent;
+import org.apache.tapestry.contrib.table.model.ITableDataModel;
+import org.apache.tapestry.contrib.table.model.ITableDataModelListener;
+
+/**
+ * An implementation of the listener support in the ITableDataModel interface
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public abstract class AbstractTableDataModel implements ITableDataModel
+{
+	private List m_arrListeners;
+
+	public AbstractTableDataModel()
+	{
+		m_arrListeners = new ArrayList();
+	}
+
+	/**
+	 * Method fireTableDataModelEvent.
+	 * Fires a change event to all listeners
+	 * @param objEvent the event to pass to the listeners
+	 */
+	protected void fireTableDataModelEvent(CTableDataModelEvent objEvent)
+	{
+        synchronized (m_arrListeners) {
+            List arrEmptyReferences = null;
+        
+    		for (Iterator it = m_arrListeners.iterator(); it.hasNext();)
+    		{
+                WeakReference objRef = (WeakReference) it.next();
+    			ITableDataModelListener objListener =
+    				(ITableDataModelListener) objRef.get();
+                if (objListener != null) 
+                    objListener.tableDataChanged(objEvent);
+                else {
+                    if (arrEmptyReferences == null)
+                        arrEmptyReferences = new ArrayList();
+                    arrEmptyReferences.add(objRef);
+                }
+    		}
+
+            if (arrEmptyReferences != null)
+                m_arrListeners.removeAll(arrEmptyReferences);
+        }
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableDataModel#addTableDataModelListener(ITableDataModelListener)
+	 */
+	public void addTableDataModelListener(ITableDataModelListener objListener)
+	{
+        synchronized (m_arrListeners) {
+    		m_arrListeners.add(new WeakReference(objListener));
+        }
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableDataModel#removeTableDataModelListener(ITableDataModelListener)
+	 */
+	public void removeTableDataModelListener(ITableDataModelListener objListener)
+	{
+        synchronized (m_arrListeners) {
+            List arrEmptyReferences = null;
+        
+            for (Iterator it = m_arrListeners.iterator(); it.hasNext();)
+            {
+                WeakReference objRef = (WeakReference) it.next();
+                ITableDataModelListener objStoredListener =
+                    (ITableDataModelListener) objRef.get();
+                if (objListener == objStoredListener || objStoredListener == null) { 
+                    if (arrEmptyReferences == null)
+                        arrEmptyReferences = new ArrayList();
+                    arrEmptyReferences.add(objRef);
+                }
+            }
+
+            if (arrEmptyReferences != null)
+                m_arrListeners.removeAll(arrEmptyReferences);
+        }
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/AbstractTableModel.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/AbstractTableModel.java
new file mode 100644
index 0000000..a8d964f
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/AbstractTableModel.java
@@ -0,0 +1,85 @@
+//  Copyright 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.tapestry.contrib.table.model.common;
+
+import java.io.Serializable;
+
+import org.apache.tapestry.contrib.table.model.ITableModel;
+import org.apache.tapestry.contrib.table.model.ITablePagingState;
+import org.apache.tapestry.contrib.table.model.ITableSortingState;
+import org.apache.tapestry.contrib.table.model.simple.SimpleTableState;
+
+/**
+ * A base table model that implements the handling of the model state.
+ * Used by most standard ITableModel implementations.
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public abstract class AbstractTableModel implements ITableModel, Serializable
+{
+    private SimpleTableState m_objTableState;
+
+    protected AbstractTableModel()
+    {
+        this(new SimpleTableState());
+    }
+
+    protected AbstractTableModel(SimpleTableState objTableState)
+    {
+        m_objTableState = objTableState;
+    }
+    
+    /**
+     * @see org.apache.tapestry.contrib.table.model.ITableModel#getPagingState()
+     */
+    public ITablePagingState getPagingState()
+    {
+        return getState().getPagingState();
+    }
+
+    /**
+     * @see org.apache.tapestry.contrib.table.model.ITableModel#getSortingState()
+     */
+    public ITableSortingState getSortingState()
+    {
+        return getState().getSortingState();
+    }
+
+    /**
+     * Returns the tableState.
+     * @return SimpleTableState
+     */
+    public SimpleTableState getState()
+    {
+        return m_objTableState;
+    }
+
+    protected abstract int getRowCount();
+    
+    public int getPageCount()
+    {
+        int nRowCount = getRowCount();
+        if (nRowCount == 0)
+            return 1;
+
+        int nPageSize = getPagingState().getPageSize();
+        if (nPageSize <= 0)
+            return 1;
+
+        return (nRowCount - 1) / nPageSize + 1;
+    }
+    
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/ArrayIterator.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/ArrayIterator.java
new file mode 100644
index 0000000..950f0dc
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/ArrayIterator.java
@@ -0,0 +1,79 @@
+//  Copyright 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.tapestry.contrib.table.model.common;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * @version $Id$
+ * @author mindbridge
+ */
+public class ArrayIterator implements Iterator
+{
+	private Object[] m_arrValues;
+	private int m_nFrom;
+	private int m_nTo;
+	private int m_nCurrent;
+
+	public ArrayIterator(Object[] arrValues)
+	{
+		this(arrValues, 0, arrValues.length);
+	}
+
+	public ArrayIterator(Object[] arrValues, int nFrom, int nTo)
+	{
+		m_arrValues = arrValues;
+		m_nFrom = nFrom;
+		m_nTo = nTo;
+
+		if (m_nFrom < 0)
+			m_nFrom = 0;
+		if (m_nTo < m_nFrom)
+			m_nTo = m_nFrom;
+		if (m_nTo > m_arrValues.length)
+			m_nTo = m_arrValues.length;
+
+		m_nCurrent = m_nFrom;
+	}
+
+	/**
+	 * @see java.util.Iterator#hasNext()
+	 */
+	public boolean hasNext()
+	{
+		return m_nCurrent < m_nTo;
+	}
+
+	/**
+	 * @see java.util.Iterator#next()
+	 */
+	public Object next()
+	{
+		//System.out.println("index: " + m_nCurrent + "   size: " + m_arrValues.length + "  to: " + m_nTo);
+		if (!hasNext())
+			throw new NoSuchElementException();
+		return m_arrValues[m_nCurrent++];
+	}
+
+	/**
+	 * @see java.util.Iterator#remove()
+	 */
+	public void remove()
+	{
+		throw new UnsupportedOperationException();
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/BasicTableModelWrap.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/BasicTableModelWrap.java
new file mode 100644
index 0000000..30e0118
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/BasicTableModelWrap.java
@@ -0,0 +1,80 @@
+//  Copyright 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.tapestry.contrib.table.model.common;
+
+import java.util.Iterator;
+
+import org.apache.tapestry.contrib.table.model.IBasicTableModel;
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+import org.apache.tapestry.contrib.table.model.ITableColumnModel;
+import org.apache.tapestry.contrib.table.model.simple.SimpleTableState;
+
+/**
+ * @version $Id$
+ * @author mindbridge
+ */
+public class BasicTableModelWrap extends AbstractTableModel 
+{
+    private IBasicTableModel m_objBasicTableModel;
+    private ITableColumnModel m_objTableColumnModel;
+
+    public BasicTableModelWrap(IBasicTableModel objBasicTableModel, ITableColumnModel objColumnModel)
+    {
+        this(objBasicTableModel, objColumnModel, new SimpleTableState());
+    }
+
+    public BasicTableModelWrap(IBasicTableModel objBasicTableModel, ITableColumnModel objColumnModel, SimpleTableState objState)
+    {
+        super(objState);
+        m_objBasicTableModel = objBasicTableModel;
+        m_objTableColumnModel = objColumnModel;
+    }
+
+    /**
+     * @see org.apache.tapestry.contrib.table.model.ITableModel#getColumnModel()
+     */
+    public ITableColumnModel getColumnModel()
+    {
+        return m_objTableColumnModel;
+    }
+
+    /**
+     * @see org.apache.tapestry.contrib.table.model.common.AbstractTableModel#getRowCount()
+     */
+    protected int getRowCount()
+    {
+        return m_objBasicTableModel.getRowCount();
+    }
+
+    /**
+     * @see org.apache.tapestry.contrib.table.model.ITableModel#getCurrentPageRows()
+     */
+    public Iterator getCurrentPageRows()
+    {
+        int nPageSize = getPagingState().getPageSize();
+        if (nPageSize <= 0)
+            nPageSize = getRowCount();
+
+        int nCurrentPage = getPagingState().getCurrentPage();
+        int nFrom = nCurrentPage * nPageSize;
+        
+        String strSortColumn = getSortingState().getSortColumn();
+        ITableColumn objSortColumn = getColumnModel().getColumn(strSortColumn); 
+        boolean bSortOrder = getSortingState().getSortOrder();
+        
+        return m_objBasicTableModel.getCurrentPageRows(nFrom, nPageSize, objSortColumn, bSortOrder);
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/BlockTableRendererSource.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/BlockTableRendererSource.java
new file mode 100644
index 0000000..5b2a99a
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/BlockTableRendererSource.java
@@ -0,0 +1,125 @@
+//  Copyright 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.tapestry.contrib.table.model.common;
+
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.components.Block;
+import org.apache.tapestry.components.BlockRenderer;
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+import org.apache.tapestry.contrib.table.model.ITableModelSource;
+import org.apache.tapestry.contrib.table.model.ITableRendererListener;
+import org.apache.tapestry.contrib.table.model.ITableRendererSource;
+import org.apache.tapestry.util.ComponentAddress;
+
+/**
+ * 
+ * @version $Id$
+ * @author mindbridge
+ * @since 2.3
+ */
+public class BlockTableRendererSource implements ITableRendererSource
+{
+	private ComponentAddress m_objBlockAddress;
+	private ComponentAddress m_objListenerAddress;
+
+	public BlockTableRendererSource(Block objBlock)
+	{
+		this(new ComponentAddress(objBlock));
+	}
+
+	public BlockTableRendererSource(
+		Block objBlock,
+		ITableRendererListener objListener)
+	{
+		this(new ComponentAddress(objBlock), new ComponentAddress(objListener));
+	}
+
+	public BlockTableRendererSource(ComponentAddress objBlockAddress)
+	{
+		this(objBlockAddress, null);
+	}
+
+	public BlockTableRendererSource(
+		ComponentAddress objBlockAddress,
+		ComponentAddress objListenerAddress)
+	{
+		setBlockAddress(objBlockAddress);
+		setListenerAddress(objListenerAddress);
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableRendererSource#getRenderer(IRequestCycle, ITableModelSource, ITableColumn, Object)
+	 */
+	public IRender getRenderer(
+		IRequestCycle objCycle,
+		ITableModelSource objSource,
+		ITableColumn objColumn,
+		Object objRow)
+	{
+		ComponentAddress objListenerAddress = getListenerAddress();
+		if (objListenerAddress != null)
+		{
+			ITableRendererListener objListener =
+				(ITableRendererListener) objListenerAddress.findComponent(
+					objCycle);
+			objListener.initializeRenderer(
+				objCycle,
+				objSource,
+				objColumn,
+				objRow);
+		}
+
+		Block objBlock = (Block) getBlockAddress().findComponent(objCycle);
+		return new BlockRenderer(objBlock);
+	}
+
+	/**
+	 * Returns the blockAddress.
+	 * @return ComponentAddress
+	 */
+	public ComponentAddress getBlockAddress()
+	{
+		return m_objBlockAddress;
+	}
+
+	/**
+	 * Sets the blockAddress.
+	 * @param blockAddress The blockAddress to set
+	 */
+	public void setBlockAddress(ComponentAddress blockAddress)
+	{
+		m_objBlockAddress = blockAddress;
+	}
+
+	/**
+	 * Returns the listenerAddress.
+	 * @return ComponentAddress
+	 */
+	public ComponentAddress getListenerAddress()
+	{
+		return m_objListenerAddress;
+	}
+
+	/**
+	 * Sets the listenerAddress.
+	 * @param listenerAddress The listenerAddress to set
+	 */
+	public void setListenerAddress(ComponentAddress listenerAddress)
+	{
+		m_objListenerAddress = listenerAddress;
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/ComponentTableRendererSource.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/ComponentTableRendererSource.java
new file mode 100644
index 0000000..4a9c2f1
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/ComponentTableRendererSource.java
@@ -0,0 +1,81 @@
+//  Copyright 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.tapestry.contrib.table.model.common;
+
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+import org.apache.tapestry.contrib.table.model.ITableModelSource;
+import org.apache.tapestry.contrib.table.model.ITableRendererListener;
+import org.apache.tapestry.contrib.table.model.ITableRendererSource;
+import org.apache.tapestry.util.ComponentAddress;
+
+/**
+ * 
+ * @version $Id$
+ * @author mindbridge
+ * @since 2.3
+ */
+public class ComponentTableRendererSource implements ITableRendererSource
+{
+	private ComponentAddress m_objComponentAddress;
+
+	public ComponentTableRendererSource(ITableRendererListener objComponent)
+	{
+		this(new ComponentAddress(objComponent));
+	}
+
+	public ComponentTableRendererSource(ComponentAddress objComponentAddress)
+	{
+		setComponentAddress(objComponentAddress);
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableRendererSource#getRenderer(IRequestCycle, ITableModelSource, ITableColumn, Object)
+	 */
+	public IRender getRenderer(
+		IRequestCycle objCycle,
+		ITableModelSource objSource,
+		ITableColumn objColumn,
+		Object objRow)
+	{
+		ITableRendererListener objComponent =
+			(ITableRendererListener) getComponentAddress().findComponent(
+				objCycle);
+
+		objComponent.initializeRenderer(objCycle, objSource, objColumn, objRow);
+
+		return objComponent;
+	}
+
+	/**
+	 * Returns the listenerAddress.
+	 * @return ComponentAddress
+	 */
+	public ComponentAddress getComponentAddress()
+	{
+		return m_objComponentAddress;
+	}
+
+	/**
+	 * Sets the listenerAddress.
+	 * @param listenerAddress The listenerAddress to set
+	 */
+	public void setComponentAddress(ComponentAddress listenerAddress)
+	{
+		m_objComponentAddress = listenerAddress;
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/FullTableSessionStateManager.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/FullTableSessionStateManager.java
new file mode 100644
index 0000000..7211bbd
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/FullTableSessionStateManager.java
@@ -0,0 +1,51 @@
+//  Copyright 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.tapestry.contrib.table.model.common;
+
+import java.io.Serializable;
+
+import org.apache.tapestry.contrib.table.model.ITableModel;
+import org.apache.tapestry.contrib.table.model.ITableSessionStateManager;
+
+/**
+ * A simple ITableSessionStateManager implementation 
+ * that saves the entire table model into the session.
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public class FullTableSessionStateManager implements ITableSessionStateManager
+{
+
+    public final static FullTableSessionStateManager FULL_STATE_MANAGER =
+        new FullTableSessionStateManager();
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableSessionStateManager#getSessionState(ITableModel)
+	 */
+	public Serializable getSessionState(ITableModel objModel)
+	{
+		return (Serializable) objModel;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableSessionStateManager#recreateTableModel(Serializable)
+	 */
+	public ITableModel recreateTableModel(Serializable objState)
+	{
+		return (ITableModel) objState;
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/NullTableSessionStateManager.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/NullTableSessionStateManager.java
new file mode 100644
index 0000000..d2921f1
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/NullTableSessionStateManager.java
@@ -0,0 +1,51 @@
+//  Copyright 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.tapestry.contrib.table.model.common;
+
+import java.io.Serializable;
+
+import org.apache.tapestry.contrib.table.model.ITableModel;
+import org.apache.tapestry.contrib.table.model.ITableSessionStateManager;
+
+/**
+ * A simple ITableSessionStateManager implementation 
+ * that saves nothing at all into the session.
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public class NullTableSessionStateManager implements ITableSessionStateManager
+{
+    
+    public final static NullTableSessionStateManager NULL_STATE_MANAGER =
+        new NullTableSessionStateManager();
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableSessionStateManager#getSessionState(ITableModel)
+	 */
+	public Serializable getSessionState(ITableModel objModel)
+	{
+		return null;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableSessionStateManager#recreateTableModel(Serializable)
+	 */
+	public ITableModel recreateTableModel(Serializable objState)
+	{
+		return null;
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/ReverseComparator.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/ReverseComparator.java
new file mode 100644
index 0000000..4622156
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/common/ReverseComparator.java
@@ -0,0 +1,41 @@
+//  Copyright 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.tapestry.contrib.table.model.common;
+
+import java.util.Comparator;
+
+/**
+ * @version $Id$
+ * @author mindbridge
+ *
+ */
+public class ReverseComparator implements Comparator
+{
+	private Comparator m_objComparator;
+
+	public ReverseComparator(Comparator objComparator)
+	{
+		m_objComparator = objComparator;
+	}
+
+	/**
+	 * @see java.util.Comparator#compare(Object, Object)
+	 */
+	public int compare(Object objValue1, Object objValue2)
+	{
+		return -m_objComparator.compare(objValue1, objValue2);
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ognl/ExpressionTableColumn.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ognl/ExpressionTableColumn.java
new file mode 100644
index 0000000..c0beb91
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ognl/ExpressionTableColumn.java
@@ -0,0 +1,49 @@
+//  Copyright 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.tapestry.contrib.table.model.ognl;
+
+import org.apache.tapestry.contrib.table.model.simple.SimpleTableColumn;
+
+/**
+ * @author mindbridge
+ *
+ */
+public class ExpressionTableColumn extends SimpleTableColumn
+{
+    public ExpressionTableColumn(String strColumnName, String strExpression)
+    {
+        this(strColumnName, strExpression, false);
+    }
+
+    public ExpressionTableColumn(String strColumnName, String strExpression, boolean bSortable)
+    {
+        this(strColumnName, strColumnName, strExpression, bSortable);
+    }
+
+    public ExpressionTableColumn(String strColumnName, String strDisplayName, String strExpression)
+    {
+        this(strColumnName, strDisplayName, strExpression, false);
+    }
+
+    public ExpressionTableColumn(
+        String strColumnName,
+        String strDisplayName,
+        String strExpression,
+        boolean bSortable)
+    {
+        super(strColumnName, strDisplayName, bSortable);
+        setEvaluator(new OgnlTableColumnEvaluator(strExpression));
+    }
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ognl/ExpressionTableColumnModel.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ognl/ExpressionTableColumnModel.java
new file mode 100644
index 0000000..04e26a9
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ognl/ExpressionTableColumnModel.java
@@ -0,0 +1,137 @@
+//  Copyright 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.tapestry.contrib.table.model.ognl;
+
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+import org.apache.tapestry.contrib.table.model.simple.SimpleTableColumnModel;
+
+/**
+ * @author mindbridge
+ *
+ */
+public class ExpressionTableColumnModel extends SimpleTableColumnModel
+{
+    /**
+     * Constructs a table column model containting OGNL expression columns. <br>
+     * The data for the columns is provided in the form of a string array,
+     * where the info of each column is stored in two consecutive fields in
+     * the array, hence its size must be even. The expected info is the following:
+     * <ul>
+     *   <li> Column Name
+     *   <li> OGNL expression
+     * </ul>
+     * @param arrColumnInfo The information to construct the columns from
+     * @param bSorted Whether all columns are sorted or not
+     */
+    public ExpressionTableColumnModel(String[] arrColumnInfo, boolean bSorted)
+    {
+        this(convertToDetailedArray(arrColumnInfo, bSorted));
+    }
+
+    /**
+     * Constructs a table column model containting OGNL expression columns. <br>
+     * The data for the columns is provided in the form of a string array,
+     * where the info of each column is stored in four consecutive fields in
+     * the array, hence its size must be divisible by 4. <br>
+     * The expected info is the following:
+     * <ul>
+     *   <li> Column Name
+     *   <li> Display Name
+     *   <li> OGNL expression
+     *   <li> Sorting of the column. This is either a Boolean, 
+     *        or a String representation of a boolean.
+     * </ul>
+     * @param arrColumnInfo
+     */
+    public ExpressionTableColumnModel(Object[] arrColumnInfo)
+    {
+        super(convertToColumns(arrColumnInfo));
+    }
+
+    /**
+     * Method convertToDetailedArray.
+     * @param arrColumnInfo
+     * @param bSorted
+     * @return Object[]
+     */
+    protected static Object[] convertToDetailedArray(String[] arrColumnInfo, boolean bSorted)
+    {
+        int nColumns = arrColumnInfo.length / 2;
+        int nSize = nColumns * 4;
+        Object[] arrDetailedInfo = new Object[nSize];
+
+        for (int i = 0; i < nColumns; i++)
+        {
+            int nInputBaseIndex = 2 * i;
+            String strColumnName = arrColumnInfo[nInputBaseIndex];
+            String strExpression = arrColumnInfo[nInputBaseIndex + 1];
+
+            int nOutputBaseIndex = 4 * i;
+            arrDetailedInfo[nOutputBaseIndex] = strColumnName;
+            arrDetailedInfo[nOutputBaseIndex + 1] = strColumnName;
+            arrDetailedInfo[nOutputBaseIndex + 2] = strExpression;
+            arrDetailedInfo[nOutputBaseIndex + 3] = bSorted ? Boolean.TRUE : Boolean.FALSE;
+        }
+
+        return arrDetailedInfo;
+    }
+
+    /**
+     * Method convertToColumns.
+     * @param arrDetailedInfo
+     * @return ITableColumn[]
+     */
+    protected static ITableColumn[] convertToColumns(Object[] arrDetailedInfo)
+    {
+        int nColumns = arrDetailedInfo.length / 4;
+        ITableColumn[] arrColumns = new ITableColumn[nColumns];
+
+        for (int i = 0; i < nColumns; i++)
+        {
+            Object objTempValue;
+            int nBaseIndex = 4 * i;
+
+            String strColumnName = "";
+            objTempValue = arrDetailedInfo[nBaseIndex];
+            if (objTempValue != null)
+                strColumnName = objTempValue.toString();
+
+            String strDisplayName = "";
+            objTempValue = arrDetailedInfo[nBaseIndex + 1];
+            if (objTempValue != null)
+                strDisplayName = objTempValue.toString();
+
+            String strExpression = "";
+            objTempValue = arrDetailedInfo[nBaseIndex + 2];
+            if (objTempValue != null)
+                strExpression = objTempValue.toString();
+
+            boolean bSorted = false;
+            objTempValue = arrDetailedInfo[nBaseIndex + 3];
+            if (objTempValue != null)
+            {
+                if (objTempValue instanceof Boolean)
+                    bSorted = ((Boolean) objTempValue).booleanValue();
+                else
+                    bSorted = Boolean.getBoolean(objTempValue.toString());
+            }
+
+            arrColumns[i] =
+                new ExpressionTableColumn(strColumnName, strDisplayName, strExpression, bSorted);
+        }
+
+        return arrColumns;
+    }
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ognl/OgnlTableColumnEvaluator.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ognl/OgnlTableColumnEvaluator.java
new file mode 100644
index 0000000..f3f6b3a
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/ognl/OgnlTableColumnEvaluator.java
@@ -0,0 +1,72 @@
+//  Copyright 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.tapestry.contrib.table.model.ognl;
+
+import ognl.Ognl;
+import ognl.OgnlException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+import org.apache.tapestry.contrib.table.model.simple.ITableColumnEvaluator;
+import org.apache.tapestry.util.prop.OgnlUtils;
+
+/**
+ * @author mindbridge
+ *
+ */
+public class OgnlTableColumnEvaluator implements ITableColumnEvaluator
+{
+	private static final Log LOG =
+		LogFactory.getLog(ExpressionTableColumn.class);
+
+	private String m_strExpression;
+	transient private Object m_objParsedExpression = null;
+
+	public OgnlTableColumnEvaluator(String strExpression)
+	{
+		m_strExpression = strExpression;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.simple.ITableColumnEvaluator#getColumnValue(ITableColumn, Object)
+	 */
+	public Object getColumnValue(ITableColumn objColumn, Object objRow)
+	{
+		// If no expression is given, then this is dummy column. Return something.
+		if (m_strExpression == null || m_strExpression.equals(""))
+			return "";
+
+			synchronized (this)
+			{
+				if (m_objParsedExpression == null)
+					m_objParsedExpression =
+						OgnlUtils.getParsedExpression(m_strExpression);
+			}
+
+		try
+		{
+			Object objValue = Ognl.getValue(m_objParsedExpression, objRow);
+			return objValue;
+		}
+		catch (OgnlException e)
+		{
+			LOG.error(
+				"Cannot use column expression '" + m_strExpression + "' in row",
+				e);
+			return "";
+		}
+	}
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/ColumnComparator.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/ColumnComparator.java
new file mode 100644
index 0000000..2002f6e
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/ColumnComparator.java
@@ -0,0 +1,65 @@
+//  Copyright 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.tapestry.contrib.table.model.simple;
+
+import java.util.Comparator;
+
+/**
+ * In order to provide more generic behaviour, ITableColumn 
+ * has no "column value" concept. The comparator it returns 
+ * compares two table rows, rather than values specific to the column. 
+ * <p>
+ * SimpleTableColumn introduces the concept of "column value" and 
+ * allows one to extract that "column value" from the row using 
+ * the getColumnValue() method. In practice comparisons are also typically 
+ * done between these values rather than the full row objects.
+ * <p>
+ * This comparator extracts the column values from the rows passed 
+ * and uses the provided comparator to compare the values.
+ * It therefore allows a comparator designed for comparing column values to be
+ * quickly wrapped and used as a comparator comparing rows, which is what
+ * ITableColumn is expected to return.
+ * <p>
+ * Example:
+ * <p>
+ * objColumn.setComparator(new ColumnComparator(objColumn, objBeanComparator));    
+ * 
+ * @version $Id$
+ * @author mindbridge
+ *
+ */
+public class ColumnComparator implements Comparator
+{
+    private SimpleTableColumn m_objColumn;
+	private Comparator m_objComparator;
+
+	public ColumnComparator(SimpleTableColumn objColumn, Comparator objComparator)
+	{
+        m_objColumn = objColumn;
+		m_objComparator = objComparator;
+	}
+
+	/**
+	 * @see java.util.Comparator#compare(Object, Object)
+	 */
+	public int compare(Object objRow1, Object objRow2)
+	{
+        Object objValue1 = m_objColumn.getColumnValue(objRow1);
+        Object objValue2 = m_objColumn.getColumnValue(objRow2);
+                
+		return m_objComparator.compare(objValue1, objValue2);
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/ITableColumnEvaluator.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/ITableColumnEvaluator.java
new file mode 100644
index 0000000..9eccdb8
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/ITableColumnEvaluator.java
@@ -0,0 +1,28 @@
+//  Copyright 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.tapestry.contrib.table.model.simple;
+
+import java.io.Serializable;
+
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+
+/**
+ * @author mindbridge
+ *
+ */
+public interface ITableColumnEvaluator extends Serializable
+{
+	Object getColumnValue(ITableColumn objColumn, Object objRow);
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleListTableDataModel.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleListTableDataModel.java
new file mode 100644
index 0000000..f9c0983
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleListTableDataModel.java
@@ -0,0 +1,143 @@
+//  Copyright 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.tapestry.contrib.table.model.simple;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.tapestry.contrib.table.model.CTableDataModelEvent;
+import org.apache.tapestry.contrib.table.model.common.AbstractTableDataModel;
+import org.apache.tapestry.contrib.table.model.common.ArrayIterator;
+
+/**
+ * A minimal list implementation of the 
+ * {@link org.apache.tapestry.contrib.table.model.ITableDataModel} interface
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public class SimpleListTableDataModel extends AbstractTableDataModel implements Serializable
+{
+	private List m_arrRows;
+
+	public SimpleListTableDataModel(Object[] arrRows)
+	{
+		this(Arrays.asList(arrRows));
+	}
+
+	public SimpleListTableDataModel(List arrRows)
+	{
+		m_arrRows = arrRows;
+	}
+
+    public SimpleListTableDataModel(Collection arrRows)
+    {
+        m_arrRows = new ArrayList(arrRows);
+    }
+
+    public SimpleListTableDataModel(Iterator objRows)
+    {
+        m_arrRows = new ArrayList();
+        CollectionUtils.addAll(m_arrRows, objRows);
+    }
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableDataModel#getRowCount()
+	 */
+	public int getRowCount()
+	{
+		return m_arrRows.size();
+	}
+
+	/**
+	 * Returns the row element at the given position
+     * @param nRow the index of the element to return
+	 */
+	public Object getRow(int nRow)
+	{
+		if (nRow < 0 || nRow >= m_arrRows.size())
+		{
+			// error message
+			return null;
+		}
+		return m_arrRows.get(nRow);
+	}
+
+	/**
+	 * Returns an Iterator with the elements from the given range
+     * @param nFrom the start of the range (inclusive)
+     * @param nTo the stop of the range (exclusive)
+	 */
+	public Iterator getRows(int nFrom, int nTo)
+	{
+		return new ArrayIterator(m_arrRows.toArray(), nFrom, nTo);
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableDataModel#getRows()
+	 */
+	public Iterator getRows()
+	{
+		return m_arrRows.iterator();
+	}
+
+	/**
+	 * Method addRow.
+     * Adds a row object to the model at its end
+	 * @param objRow the row object to add
+	 */
+	public void addRow(Object objRow)
+	{
+		m_arrRows.add(objRow);
+
+		CTableDataModelEvent objEvent = new CTableDataModelEvent();
+		fireTableDataModelEvent(objEvent);
+	}
+
+    public void addRows(Collection arrRows)
+    {
+        m_arrRows.addAll(arrRows);
+
+        CTableDataModelEvent objEvent = new CTableDataModelEvent();
+        fireTableDataModelEvent(objEvent);
+    }
+
+	/**
+	 * Method removeRow.
+     * Removes a row object from the model
+	 * @param objRow the row object to remove
+	 */
+	public void removeRow(Object objRow)
+	{
+		m_arrRows.remove(objRow);
+
+		CTableDataModelEvent objEvent = new CTableDataModelEvent();
+		fireTableDataModelEvent(objEvent);
+	}
+
+    public void removeRows(Collection arrRows)
+    {
+        m_arrRows.removeAll(arrRows);
+
+        CTableDataModelEvent objEvent = new CTableDataModelEvent();
+        fireTableDataModelEvent(objEvent);
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleSetTableDataModel.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleSetTableDataModel.java
new file mode 100644
index 0000000..fbe6fe6
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleSetTableDataModel.java
@@ -0,0 +1,101 @@
+//  Copyright 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.tapestry.contrib.table.model.simple;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Set;
+
+import org.apache.tapestry.contrib.table.model.CTableDataModelEvent;
+import org.apache.tapestry.contrib.table.model.common.AbstractTableDataModel;
+
+/**
+ * A minimal set implementation of the 
+ * {@link org.apache.tapestry.contrib.table.model.ITableDataModel} interface
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public class SimpleSetTableDataModel extends AbstractTableDataModel implements Serializable
+{
+    private Set m_setRows;
+
+    public SimpleSetTableDataModel(Set setRows)
+    {
+        m_setRows = setRows;
+    }
+
+    /**
+     * @see org.apache.tapestry.contrib.table.model.ITableDataModel#getRowCount()
+     */
+    public int getRowCount()
+    {
+        return m_setRows.size();
+    }
+
+    /**
+     * @see org.apache.tapestry.contrib.table.model.ITableDataModel#getRows()
+     */
+    public Iterator getRows()
+    {
+        return m_setRows.iterator();
+    }
+
+    /**
+     * Method addRow.
+     * Adds a row object to the model at its end
+     * @param objRow the row object to add
+     */
+    public void addRow(Object objRow)
+    {
+        if (m_setRows.contains(objRow)) return;
+        m_setRows.add(objRow);
+
+        CTableDataModelEvent objEvent = new CTableDataModelEvent();
+        fireTableDataModelEvent(objEvent);
+    }
+
+    public void addRows(Collection arrRows)
+    {
+        m_setRows.addAll(arrRows);
+
+        CTableDataModelEvent objEvent = new CTableDataModelEvent();
+        fireTableDataModelEvent(objEvent);
+    }
+
+    /**
+     * Method removeRow.
+     * Removes a row object from the model
+     * @param objRow the row object to remove
+     */
+    public void removeRow(Object objRow)
+    {
+        if (!m_setRows.contains(objRow)) return;
+        m_setRows.remove(objRow);
+
+        CTableDataModelEvent objEvent = new CTableDataModelEvent();
+        fireTableDataModelEvent(objEvent);
+    }
+
+    public void removeRows(Collection arrRows)
+    {
+        m_setRows.removeAll(arrRows);
+
+        CTableDataModelEvent objEvent = new CTableDataModelEvent();
+        fireTableDataModelEvent(objEvent);
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableColumn.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableColumn.java
new file mode 100644
index 0000000..aeb8513
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableColumn.java
@@ -0,0 +1,235 @@
+//  Copyright 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.tapestry.contrib.table.model.simple;
+
+import java.io.Serializable;
+import java.util.Comparator;
+
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.contrib.table.model.ITableRendererSource;
+import org.apache.tapestry.contrib.table.model.common.AbstractTableColumn;
+
+/**
+ * A simple minimal implementation of the 
+ * {@link org.apache.tapestry.contrib.table.model.ITableColumn} interface that
+ * provides all the basic services for displaying a column.
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public class SimpleTableColumn extends AbstractTableColumn
+{
+    public static final ITableRendererSource DEFAULT_COLUMN_RENDERER_SOURCE = 
+        new SimpleTableColumnRendererSource();
+
+    public static final ITableRendererSource FORM_COLUMN_RENDERER_SOURCE = 
+        new SimpleTableColumnFormRendererSource();
+
+    public static final ITableRendererSource DEFAULT_VALUE_RENDERER_SOURCE = 
+        new SimpleTableValueRendererSource();
+
+	private String m_strDisplayName;
+	private ITableColumnEvaluator m_objEvaluator;
+
+	/**
+	 * Creates a SimpleTableColumn
+	 * @param strColumnName the identifying name and display name of the column
+	 */
+	public SimpleTableColumn(String strColumnName)
+	{
+		this(strColumnName, strColumnName);
+	}
+
+	/**
+	 * Creates a SimpleTableColumn
+	 * @param strColumnName the identifying name and display name of the column
+	 * @param bSortable whether the column is sortable
+	 */
+	public SimpleTableColumn(String strColumnName, boolean bSortable)
+	{
+		this(strColumnName, strColumnName, bSortable);
+	}
+
+	/**
+	 * Creates a SimpleTableColumn
+	 * @param strColumnName the identifying name and display name of the column
+	 * @param bSortable whether the column is sortable
+     * @param objEvaluator the evaluator to extract the column value from the row
+	 */
+	public SimpleTableColumn(
+		String strColumnName,
+        ITableColumnEvaluator objEvaluator,
+		boolean bSortable)
+	{
+		this(strColumnName, strColumnName, objEvaluator, bSortable);
+	}
+
+	/**
+	 * Creates a SimpleTableColumn
+	 * @param strColumnName the identifying name of the column
+	 * @param strDisplayName the display name of the column
+	 */
+	public SimpleTableColumn(String strColumnName, String strDisplayName)
+	{
+		this(strColumnName, strDisplayName, false);
+	}
+
+	/**
+	 * Creates a SimpleTableColumn
+	 * @param strColumnName the identifying name of the column
+	 * @param strDisplayName the display name of the column
+	 * @param bSortable whether the column is sortable
+	 */
+	public SimpleTableColumn(
+		String strColumnName,
+		String strDisplayName,
+		boolean bSortable)
+	{
+		this(strColumnName, strDisplayName, null, bSortable);
+	}
+
+	/**
+	 * Creates a SimpleTableColumn
+	 * @param strColumnName the identifying name of the column
+	 * @param strDisplayName the display name of the column
+	 * @param bSortable whether the column is sortable
+     * @param objEvaluator the evaluator to extract the column value from the row
+	 */
+	public SimpleTableColumn(
+		String strColumnName,
+		String strDisplayName,
+		ITableColumnEvaluator objEvaluator,
+		boolean bSortable)
+	{
+		super(strColumnName, bSortable, null);
+		setComparator(new DefaultTableComparator());
+		setDisplayName(strDisplayName);
+		setColumnRendererSource(DEFAULT_COLUMN_RENDERER_SOURCE);
+		setValueRendererSource(DEFAULT_VALUE_RENDERER_SOURCE);
+		setEvaluator(objEvaluator);
+	}
+
+	/**
+	 * Returns the display name of the column that will be used 
+	 * in the table header.
+	 * Override for internationalization.
+	 * @return String the display name of the column
+	 */
+	public String getDisplayName()
+	{
+		return m_strDisplayName;
+	}
+
+	/**
+	 * Sets the displayName.
+	 * @param displayName The displayName to set
+	 */
+	public void setDisplayName(String displayName)
+	{
+		m_strDisplayName = displayName;
+	}
+
+	/**
+	 * Returns the evaluator.
+	 * @return ITableColumnEvaluator
+	 */
+	public ITableColumnEvaluator getEvaluator()
+	{
+		return m_objEvaluator;
+	}
+
+	/**
+	 * Sets the evaluator.
+	 * @param evaluator The evaluator to set
+	 */
+	public void setEvaluator(ITableColumnEvaluator evaluator)
+	{
+		m_objEvaluator = evaluator;
+	}
+
+    /**
+     * Sets a comparator that compares the values of this column rather than 
+     * the objects representing the full rows. <br>
+     * This method allows easier use of standard comparators for sorting
+     * the column. It simply wraps the provided comparator with a row-to-column 
+     * convertor and invokes the setComparator() method.
+     * @param comparator The column value comparator
+     */
+    public void setColumnComparator(Comparator comparator)
+    {
+        setComparator(new ColumnComparator(this, comparator));
+    }
+
+	/**
+	 * Extracts the value of the column from the row object
+	 * @param objRow the row object
+	 * @return Object the column value
+	 */
+	public Object getColumnValue(Object objRow)
+	{
+		ITableColumnEvaluator objEvaluator = getEvaluator();
+		if (objEvaluator != null)
+			return objEvaluator.getColumnValue(this, objRow);
+
+		// default fallback
+		return objRow.toString();
+	}
+
+    /**
+     *  Use the column name to get the display name, as well as 
+     *  the column and value renderer sources from the provided component.
+     *   
+     *  @param objSettingsContainer the component from which to get the settings 
+     */
+    public void loadSettings(IComponent objSettingsContainer)
+    {
+        String strDisplayName = objSettingsContainer.getMessages().getMessage(getColumnName(), null);
+        if (strDisplayName != null)
+            setDisplayName(strDisplayName);
+        
+        super.loadSettings(objSettingsContainer);
+    }
+
+
+	public class DefaultTableComparator implements Comparator, Serializable
+	{
+		public int compare(Object objRow1, Object objRow2)
+		{
+			Object objValue1 = getColumnValue(objRow1);
+			Object objValue2 = getColumnValue(objRow2);
+
+            if (objValue1 == objValue2)
+                return 0;
+
+            boolean bComparable1 = objValue1 instanceof Comparable;
+            boolean bComparable2 = objValue2 instanceof Comparable;
+                              
+            // non-comparable values are considered equal 
+			if (!bComparable1 && !bComparable2)
+				return 0;
+
+            // non-comparable values (null included) are considered smaller 
+            // than the comparable ones
+            if (!bComparable1)
+                return -1;
+
+            if (!bComparable2)
+                return 1;
+
+			return ((Comparable) objValue1).compareTo(objValue2);
+		}
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableColumnFormRendererSource.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableColumnFormRendererSource.java
new file mode 100644
index 0000000..8c43b9a
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableColumnFormRendererSource.java
@@ -0,0 +1,75 @@
+//  Copyright 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.tapestry.contrib.table.model.simple;
+
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+import org.apache.tapestry.contrib.table.model.ITableModelSource;
+import org.apache.tapestry.contrib.table.model.ITableRendererSource;
+import org.apache.tapestry.contrib.table.model.common.ComponentTableRendererSource;
+import org.apache.tapestry.util.ComponentAddress;
+
+/**
+ * This is a simple implementation of 
+ * {@link org.apache.tapestry.contrib.table.model.ITableRendererSource} 
+ * that returns a standard renderer of a column header. <p>
+ * 
+ * This implementation requires that the column passed is of type SimpleTableColumn
+ * 
+ * @see org.apache.tapestry.contrib.table.model.common.AbstractTableColumn
+ * @version $Id$
+ * @author mindbridge
+ * @since 2.3
+ */
+public class SimpleTableColumnFormRendererSource implements ITableRendererSource
+{
+	private ComponentTableRendererSource m_objComponentRenderer;
+
+	public SimpleTableColumnFormRendererSource()
+	{
+		m_objComponentRenderer = null;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableRendererSource#getRenderer(IRequestCycle, ITableModelSource, ITableColumn, Object)
+	 */
+	public IRender getRenderer(
+		IRequestCycle objCycle,
+		ITableModelSource objSource,
+		ITableColumn objColumn,
+		Object objRow)
+	{
+			synchronized (this)
+			{
+				if (m_objComponentRenderer == null)
+				{
+					ComponentAddress objAddress =
+						new ComponentAddress(
+							objSource.getNamespace(),
+							"SimpleTableColumnPage",
+							"tableColumnFormComponent");
+					m_objComponentRenderer =
+						new ComponentTableRendererSource(objAddress);
+				}
+			}
+
+		return m_objComponentRenderer.getRenderer(
+			objCycle,
+			objSource,
+			objColumn,
+			objRow);
+	}
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableColumnModel.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableColumnModel.java
new file mode 100644
index 0000000..7c519db
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableColumnModel.java
@@ -0,0 +1,80 @@
+//  Copyright 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.tapestry.contrib.table.model.simple;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+import org.apache.tapestry.contrib.table.model.ITableColumnModel;
+import org.apache.tapestry.contrib.table.model.common.ArrayIterator;
+
+/**
+ * A minimal implementation of the 
+ * {@link org.apache.tapestry.contrib.table.model.ITableColumnModel} interface
+ * that stores columns as an array.
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public class SimpleTableColumnModel implements ITableColumnModel, Serializable
+{
+
+    private ITableColumn[] m_arrColumns;
+    private Map m_mapColumns;
+
+    public SimpleTableColumnModel(ITableColumn[] arrColumns)
+    {
+        m_arrColumns = arrColumns;
+
+        m_mapColumns = new HashMap();
+        for (int i = 0; i < m_arrColumns.length; i++)
+            m_mapColumns.put(m_arrColumns[i].getColumnName(), m_arrColumns[i]);
+    }
+
+    public SimpleTableColumnModel(List arrColumns)
+    {
+        this((ITableColumn[]) arrColumns.toArray(new ITableColumn[arrColumns.size()]));
+    }
+
+    public int getColumnCount()
+    {
+        return m_arrColumns.length;
+    }
+
+    public ITableColumn getColumn(int nColumn)
+    {
+        if (nColumn < 0 || nColumn >= m_arrColumns.length)
+        {
+            // error message
+            return null;
+        }
+        return m_arrColumns[nColumn];
+    }
+
+    public ITableColumn getColumn(String strColumn)
+    {
+        return (ITableColumn) m_mapColumns.get(strColumn);
+    }
+
+    public Iterator getColumns()
+    {
+        return new ArrayIterator(m_arrColumns);
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableColumnRendererSource.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableColumnRendererSource.java
new file mode 100644
index 0000000..ee0658e
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableColumnRendererSource.java
@@ -0,0 +1,75 @@
+//  Copyright 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.tapestry.contrib.table.model.simple;
+
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+import org.apache.tapestry.contrib.table.model.ITableModelSource;
+import org.apache.tapestry.contrib.table.model.ITableRendererSource;
+import org.apache.tapestry.contrib.table.model.common.ComponentTableRendererSource;
+import org.apache.tapestry.util.ComponentAddress;
+
+/**
+ * This is a simple implementation of 
+ * {@link org.apache.tapestry.contrib.table.model.ITableRendererSource} 
+ * that returns a standard renderer of a column header. <p>
+ * 
+ * This implementation requires that the column passed is of type SimpleTableColumn
+ * 
+ * @see org.apache.tapestry.contrib.table.model.common.AbstractTableColumn
+ * @version $Id$
+ * @author mindbridge
+ * @since 2.3
+ */
+public class SimpleTableColumnRendererSource implements ITableRendererSource
+{
+	private ComponentTableRendererSource m_objComponentRenderer;
+
+	public SimpleTableColumnRendererSource()
+	{
+		m_objComponentRenderer = null;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableRendererSource#getRenderer(IRequestCycle, ITableModelSource, ITableColumn, Object)
+	 */
+	public IRender getRenderer(
+		IRequestCycle objCycle,
+		ITableModelSource objSource,
+		ITableColumn objColumn,
+		Object objRow)
+	{
+			synchronized (this)
+			{
+				if (m_objComponentRenderer == null)
+				{
+					ComponentAddress objAddress =
+						new ComponentAddress(
+							objSource.getNamespace(),
+							"SimpleTableColumnPage",
+							"tableColumnComponent");
+					m_objComponentRenderer =
+						new ComponentTableRendererSource(objAddress);
+				}
+			}
+
+		return m_objComponentRenderer.getRenderer(
+			objCycle,
+			objSource,
+			objColumn,
+			objRow);
+	}
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableModel.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableModel.java
new file mode 100644
index 0000000..f1cd13d
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableModel.java
@@ -0,0 +1,180 @@
+//  Copyright 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.tapestry.contrib.table.model.simple;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Iterator;
+
+import org.apache.tapestry.contrib.table.model.CTableDataModelEvent;
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+import org.apache.tapestry.contrib.table.model.ITableColumnModel;
+import org.apache.tapestry.contrib.table.model.ITableDataModel;
+import org.apache.tapestry.contrib.table.model.ITableDataModelListener;
+import org.apache.tapestry.contrib.table.model.ITableSortingState;
+import org.apache.tapestry.contrib.table.model.common.AbstractTableModel;
+import org.apache.tapestry.contrib.table.model.common.ArrayIterator;
+import org.apache.tapestry.contrib.table.model.common.ReverseComparator;
+
+/**
+ * A simple generic table model implementation.
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public class SimpleTableModel extends AbstractTableModel implements ITableDataModelListener
+{
+    private ITableDataModel m_objDataModel = null;
+    private Object[] m_arrRows = null;
+    private ITableColumnModel m_objColumnModel = null;
+
+    private SimpleTableSortingState m_objLastSortingState;
+
+    public SimpleTableModel(Object[] arrData, ITableColumn[] arrColumns)
+    {
+        this(new SimpleListTableDataModel(arrData), new SimpleTableColumnModel(arrColumns));
+    }
+
+    public SimpleTableModel(Object[] arrData, ITableColumnModel objColumnModel)
+    {
+        this(new SimpleListTableDataModel(arrData), objColumnModel);
+    }
+
+    public SimpleTableModel(ITableDataModel objDataModel, ITableColumnModel objColumnModel)
+    {
+        this(objDataModel, objColumnModel, new SimpleTableState());
+    }
+
+    public SimpleTableModel(ITableDataModel objDataModel, ITableColumnModel objColumnModel, SimpleTableState objState)
+    {
+        super(objState);
+        
+        m_arrRows = null;
+        m_objColumnModel = objColumnModel;
+        m_objLastSortingState = new SimpleTableSortingState();
+
+        setDataModel(objDataModel);
+    }
+
+    public ITableColumnModel getColumnModel()
+    {
+        return m_objColumnModel;
+    }
+
+    public Iterator getCurrentPageRows()
+    {
+        sortRows();
+
+        int nPageSize = getPagingState().getPageSize();
+        if (nPageSize <= 0)
+            return new ArrayIterator(m_arrRows);
+
+        int nCurrentPage = getPagingState().getCurrentPage();
+        int nFrom = nCurrentPage * nPageSize;
+        int nTo = (nCurrentPage + 1) * nPageSize;
+
+        return new ArrayIterator(m_arrRows, nFrom, nTo);
+    }
+
+    public int getRowCount()
+    {
+        updateRows();
+        return m_arrRows.length;
+    }
+
+    private void updateRows()
+    {
+        // If it is not null, then there is no need to extract the data
+        if (m_arrRows != null)
+            return;
+
+        // Extract the data from the model
+        m_objLastSortingState = new SimpleTableSortingState();
+
+        int nRowCount = m_objDataModel.getRowCount();
+        Object[] arrRows = new Object[nRowCount];
+
+        int i = 0;
+        for (Iterator it = m_objDataModel.getRows(); it.hasNext();)
+            arrRows[i++] = it.next();
+
+        m_arrRows = arrRows;
+    }
+
+    protected void sortRows()
+    {
+        updateRows();
+
+        ITableSortingState objSortingState = getSortingState();
+
+        // see if there is sorting required
+        String strSortColumn = objSortingState.getSortColumn();
+        if (strSortColumn == null)
+            return;
+
+        boolean bSortOrder = objSortingState.getSortOrder();
+
+        // See if the table is already sorted this way. If so, return.
+        if (strSortColumn.equals(m_objLastSortingState.getSortColumn())
+            && m_objLastSortingState.getSortOrder() == bSortOrder)
+            return;
+
+        ITableColumn objColumn = getColumnModel().getColumn(strSortColumn);
+        if (objColumn == null || !objColumn.getSortable())
+            return;
+
+        Comparator objCmp = objColumn.getComparator();
+        if (objCmp == null)
+            return;
+
+        // Okay, we have everything in place. Sort the rows.
+        if (bSortOrder == ITableSortingState.SORT_DESCENDING)
+            objCmp = new ReverseComparator(objCmp);
+
+        Arrays.sort(m_arrRows, objCmp);
+
+        m_objLastSortingState.setSortColumn(strSortColumn, bSortOrder);
+    }
+
+    public void tableDataChanged(CTableDataModelEvent objEvent)
+    {
+        m_arrRows = null;
+    }
+
+    /**
+     * Returns the dataModel.
+     * @return ITableDataModel
+     */
+    public ITableDataModel getDataModel()
+    {
+        return m_objDataModel;
+    }
+
+    /**
+     * Sets the dataModel.
+     * @param dataModel The dataModel to set
+     */
+    public void setDataModel(ITableDataModel dataModel)
+    {
+        if (m_objDataModel != null)
+            m_objDataModel.removeTableDataModelListener(this);
+            
+        m_objDataModel = dataModel;
+        m_objDataModel.addTableDataModelListener(this);
+        
+        m_arrRows = null;
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTablePagingState.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTablePagingState.java
new file mode 100644
index 0000000..5968e6c
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTablePagingState.java
@@ -0,0 +1,77 @@
+//  Copyright 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.tapestry.contrib.table.model.simple;
+
+import java.io.Serializable;
+
+import org.apache.tapestry.contrib.table.model.ITablePagingState;
+
+/**
+ * A minimal implementation of 
+ * {@link org.apache.tapestry.contrib.table.model.ITablePagingState}.
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public class SimpleTablePagingState implements ITablePagingState, Serializable
+{
+    private final static int DEFAULT_PAGE_SIZE = 10;
+
+    private int m_nPageSize;
+    private int m_nCurrentPage;
+
+    public SimpleTablePagingState()
+    {
+        m_nPageSize = DEFAULT_PAGE_SIZE;
+        m_nCurrentPage = 0;
+    }
+
+    /**
+     * Returns the pageSize.
+     * @return int
+     */
+    public int getPageSize()
+    {
+        return m_nPageSize;
+    }
+
+    /**
+     * Sets the pageSize.
+     * @param pageSize The pageSize to set
+     */
+    public void setPageSize(int pageSize)
+    {
+        m_nPageSize = pageSize;
+    }
+
+    /**
+     * Returns the currentPage.
+     * @return int
+     */
+    public int getCurrentPage()
+    {
+        return m_nCurrentPage;
+    }
+
+    /**
+     * Sets the currentPage.
+     * @param currentPage The currentPage to set
+     */
+    public void setCurrentPage(int currentPage)
+    {
+        m_nCurrentPage = currentPage;
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableSessionStateManager.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableSessionStateManager.java
new file mode 100644
index 0000000..65e0b9f
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableSessionStateManager.java
@@ -0,0 +1,69 @@
+//  Copyright 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.tapestry.contrib.table.model.simple;
+
+import java.io.Serializable;
+
+import org.apache.tapestry.contrib.table.model.ITableColumnModel;
+import org.apache.tapestry.contrib.table.model.ITableDataModel;
+import org.apache.tapestry.contrib.table.model.ITableModel;
+import org.apache.tapestry.contrib.table.model.ITableSessionStateManager;
+
+/**
+ * A {@link org.apache.tapestry.contrib.table.model.ITableSessionStateManager} 
+ * implementation that saves only the paging and sorting state of the table model 
+ * into the session.
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public class SimpleTableSessionStateManager
+	implements ITableSessionStateManager
+{
+	private ITableDataModel m_objDataModel;
+	private ITableColumnModel m_objColumnModel;
+
+	public SimpleTableSessionStateManager(
+		ITableDataModel objDataModel,
+		ITableColumnModel objColumnModel)
+	{
+		m_objDataModel = objDataModel;
+		m_objColumnModel = objColumnModel;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableSessionStateManager#getSessionState(ITableModel)
+	 */
+	public Serializable getSessionState(ITableModel objModel)
+	{
+		SimpleTableModel objSimpleModel = (SimpleTableModel) objModel;
+		return objSimpleModel.getState();
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableSessionStateManager#recreateTableModel(Serializable)
+	 */
+	public ITableModel recreateTableModel(Serializable objState)
+	{
+		if (objState == null)
+			return null;
+		SimpleTableState objSimpleState = (SimpleTableState) objState;
+		return new SimpleTableModel(
+			m_objDataModel,
+			m_objColumnModel,
+			objSimpleState);
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableSortingState.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableSortingState.java
new file mode 100644
index 0000000..89d5307
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableSortingState.java
@@ -0,0 +1,69 @@
+//  Copyright 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.tapestry.contrib.table.model.simple;
+
+import java.io.Serializable;
+
+import org.apache.tapestry.contrib.table.model.ITableSortingState;
+
+/**
+ * A minimal implementation of 
+ * {@link org.apache.tapestry.contrib.table.model.ITableSortingState}
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public class SimpleTableSortingState
+	implements ITableSortingState, Serializable
+{
+	private String m_strSortColumn;
+	private boolean m_bSortOrder;
+
+	public SimpleTableSortingState()
+	{
+		m_strSortColumn = null; // no sorting
+		m_bSortOrder = ITableSortingState.SORT_ASCENDING;
+		// irrelevant, but anyway
+	}
+
+	/**
+	 * Returns the SortOrder.
+	 * @return boolean
+	 */
+	public boolean getSortOrder()
+	{
+		return m_bSortOrder;
+	}
+
+	/**
+	 * Returns the SortColumn.
+	 * @return int
+	 */
+	public String getSortColumn()
+	{
+		return m_strSortColumn;
+	}
+
+	/**
+	 * Sets the SortColumn.
+	 * @param strSortColumn The SortColumn to set
+	 */
+	public void setSortColumn(String strSortColumn, boolean bSortOrder)
+	{
+		m_strSortColumn = strSortColumn;
+		m_bSortOrder = bSortOrder;
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableState.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableState.java
new file mode 100644
index 0000000..628df07
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableState.java
@@ -0,0 +1,64 @@
+//  Copyright 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.tapestry.contrib.table.model.simple;
+
+import java.io.Serializable;
+
+import org.apache.tapestry.contrib.table.model.ITablePagingState;
+import org.apache.tapestry.contrib.table.model.ITableSortingState;
+
+/**
+ * A container holding all of the table model states.
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public class SimpleTableState implements Serializable
+{
+	private ITablePagingState m_objPagingState;
+	private ITableSortingState m_objSortingState;
+
+	public SimpleTableState()
+	{
+		this(new SimpleTablePagingState(), new SimpleTableSortingState());
+	}
+
+	public SimpleTableState(
+		ITablePagingState objPagingState,
+		ITableSortingState objSortingState)
+	{
+		m_objPagingState = objPagingState;
+		m_objSortingState = objSortingState;
+	}
+
+	/**
+	 * Returns the pagingState.
+	 * @return ITablePagingState
+	 */
+	public ITablePagingState getPagingState()
+	{
+		return m_objPagingState;
+	}
+
+	/**
+	 * Returns the sortingState.
+	 * @return ITableSortingState
+	 */
+	public ITableSortingState getSortingState()
+	{
+		return m_objSortingState;
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableValueRendererSource.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableValueRendererSource.java
new file mode 100644
index 0000000..5214c43
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/simple/SimpleTableValueRendererSource.java
@@ -0,0 +1,63 @@
+//  Copyright 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.tapestry.contrib.table.model.simple;
+
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+import org.apache.tapestry.contrib.table.model.ITableModelSource;
+import org.apache.tapestry.contrib.table.model.ITableRendererSource;
+import org.apache.tapestry.valid.RenderString;
+
+/**
+ * This is a simple implementation of 
+ * {@link org.apache.tapestry.contrib.table.model.ITableRendererSource} 
+ * that returns a standard renderer of a column value.
+ * 
+ * This implementation requires that the column passed is of type SimpleTableColumn
+ * 
+ * @see org.apache.tapestry.contrib.table.model.common.AbstractTableColumn
+ * @version $Id$
+ * @author mindbridge
+ * @since 2.3
+ */
+public class SimpleTableValueRendererSource implements ITableRendererSource
+{
+    /** 
+     *  The representation of null values. This is geared towards HTML, but will
+     *  work for some other *ML languages as well. In any case, changing the 
+     *  column's value renderer allows selecting fully custom rendering behaviour. 
+     **/ 
+    private static final String EMPTY_REPRESENTATION = "&nbsp;";
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableRendererSource#getRenderer(IRequestCycle, ITableModelSource, ITableColumn, Object)
+	 */
+	public IRender getRenderer(
+		IRequestCycle objCycle,
+		ITableModelSource objSource,
+		ITableColumn objColumn,
+		Object objRow)
+	{
+		SimpleTableColumn objSimpleColumn = (SimpleTableColumn) objColumn;
+
+		Object objValue = objSimpleColumn.getColumnValue(objRow);
+		if (objValue == null)
+			return new RenderString(EMPTY_REPRESENTATION, true);
+
+		return new RenderString(objValue.toString());
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/ISqlConnectionSource.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/ISqlConnectionSource.java
new file mode 100644
index 0000000..fe2ab44
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/ISqlConnectionSource.java
@@ -0,0 +1,29 @@
+//  Copyright 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.tapestry.contrib.table.model.sql;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+
+/**
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public interface ISqlConnectionSource
+{
+	Connection obtainConnection() throws SQLException;
+	void returnConnection(Connection objConnection);
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/ISqlTableDataSource.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/ISqlTableDataSource.java
new file mode 100644
index 0000000..3ea4abd
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/ISqlTableDataSource.java
@@ -0,0 +1,35 @@
+//  Copyright 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.tapestry.contrib.table.model.sql;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import org.apache.tapestry.contrib.table.model.simple.SimpleTableState;
+
+/**
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public interface ISqlTableDataSource
+{
+	int getRowCount() throws SQLException;
+	ResultSet getCurrentRows(
+		SqlTableColumnModel objColumnModel,
+		SimpleTableState objState)
+		throws SQLException;
+	void closeResultSet(ResultSet objResultSet);
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/ResultSetIterator.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/ResultSetIterator.java
new file mode 100644
index 0000000..87174d5
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/ResultSetIterator.java
@@ -0,0 +1,123 @@
+//  Copyright 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.tapestry.contrib.table.model.sql;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Iterator;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public class ResultSetIterator implements Iterator
+{
+	private static final Log LOG = LogFactory.getLog(ResultSetIterator.class);
+
+	private ResultSet m_objResultSet;
+	private boolean m_bFetched;
+	private boolean m_bAvailable;
+
+	public ResultSetIterator(ResultSet objResultSet)
+	{
+		m_objResultSet = objResultSet;
+		m_bFetched = false;
+	}
+
+	/**
+	 * @see java.util.Iterator#hasNext()
+	 */
+	public synchronized boolean hasNext()
+	{
+        if (getResultSet() == null) return false;
+        
+		if (!m_bFetched)
+		{
+			m_bFetched = true;
+
+			try
+			{
+				m_bAvailable = !getResultSet().isLast();
+			}
+			catch (SQLException e)
+			{
+				LOG.warn(
+					"SQLException while testing for end of the ResultSet",
+					e);
+				m_bAvailable = false;
+			}
+
+			if (!m_bAvailable)
+				notifyEnd();
+		}
+
+		return m_bAvailable;
+	}
+
+	/**
+	 * @see java.util.Iterator#next()
+	 */
+	public synchronized Object next()
+	{
+		ResultSet objResultSet = getResultSet();
+
+		try
+		{
+			if (!objResultSet.next())
+				return null;
+		}
+		catch (SQLException e)
+		{
+			LOG.warn("SQLException while iterating over the ResultSet", e);
+			return null;
+		}
+
+		m_bFetched = false;
+		return objResultSet;
+	}
+
+	/**
+	 * @see java.util.Iterator#remove()
+	 */
+	public void remove()
+	{
+		try
+		{
+			getResultSet().deleteRow();
+		}
+		catch (SQLException e)
+		{
+			LOG.error("Cannot delete record", e);
+		}
+	}
+
+	/**
+	 * Returns the resultSet.
+	 * @return ResultSet
+	 */
+	public ResultSet getResultSet()
+	{
+		return m_objResultSet;
+	}
+
+	protected void notifyEnd()
+	{
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/SimpleSqlConnectionSource.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/SimpleSqlConnectionSource.java
new file mode 100644
index 0000000..9b64253
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/SimpleSqlConnectionSource.java
@@ -0,0 +1,79 @@
+//  Copyright 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.tapestry.contrib.table.model.sql;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * 
+ * @version $Id : $
+ * @author mindbridge
+ */
+public class SimpleSqlConnectionSource implements ISqlConnectionSource
+{
+	private static final Log LOG =
+		LogFactory.getLog(SimpleSqlConnectionSource.class);
+
+	private String m_strUrl;
+	private String m_strUser;
+	private String m_strPwd;
+
+	public SimpleSqlConnectionSource(String strUrl)
+	{
+		this(strUrl, null, null);
+	}
+
+	public SimpleSqlConnectionSource(
+		String strUrl,
+		String strUser,
+		String strPwd)
+	{
+		m_strUrl = strUrl;
+		m_strUser = strUser;
+		m_strPwd = strPwd;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.sql.ISqlConnectionSource#obtainConnection()
+	 */
+	public Connection obtainConnection() throws SQLException
+	{
+		if (m_strUser == null)
+			return DriverManager.getConnection(m_strUrl);
+		else
+			return DriverManager.getConnection(m_strUrl, m_strUser, m_strPwd);
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.sql.ISqlConnectionSource#returnConnection(Connection)
+	 */
+	public void returnConnection(Connection objConnection)
+	{
+		try
+		{
+			objConnection.close();
+		}
+		catch (SQLException e)
+		{
+			LOG.warn("Could not close connection", e);
+		}
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/SimpleSqlTableDataSource.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/SimpleSqlTableDataSource.java
new file mode 100644
index 0000000..224a6b2
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/SimpleSqlTableDataSource.java
@@ -0,0 +1,266 @@
+//  Copyright 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.tapestry.contrib.table.model.sql;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.contrib.table.model.ITablePagingState;
+import org.apache.tapestry.contrib.table.model.ITableSortingState;
+import org.apache.tapestry.contrib.table.model.simple.SimpleTableState;
+
+/**
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public class SimpleSqlTableDataSource implements ISqlTableDataSource
+{
+	private static final Log LOG =
+		LogFactory.getLog(SimpleSqlTableDataSource.class);
+
+	private ISqlConnectionSource m_objConnSource;
+	private String m_strTableName;
+	private String m_strWhereClause;
+
+	public SimpleSqlTableDataSource(
+		ISqlConnectionSource objConnSource,
+		String strTableName)
+	{
+		this(objConnSource, strTableName, null);
+	}
+
+	public SimpleSqlTableDataSource(
+		ISqlConnectionSource objConnSource,
+		String strTableName,
+		String strWhereClause)
+	{
+		setConnSource(objConnSource);
+		setTableName(strTableName);
+		setWhereClause(strWhereClause);
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.sql.ISqlTableDataSource#getRowCount()
+	 */
+	public int getRowCount() throws SQLException
+	{
+		String strQuery = generateCountQuery();
+		LOG.trace("Invoking query to count rows: " + strQuery);
+
+		Connection objConn = getConnSource().obtainConnection();
+		try
+		{
+			Statement objStmt = objConn.createStatement();
+			try
+			{
+				ResultSet objRS = objStmt.executeQuery(strQuery);
+				objRS.next();
+				return objRS.getInt(1);
+			}
+			finally
+			{
+				objStmt.close();
+			}
+		}
+		finally
+		{
+			getConnSource().returnConnection(objConn);
+		}
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.sql.ISqlTableDataSource#getCurrentRows(SqlTableColumnModel, SimpleTableState)
+	 */
+	public ResultSet getCurrentRows(
+		SqlTableColumnModel objColumnModel,
+		SimpleTableState objState)
+		throws SQLException
+	{
+		String strQuery = generateDataQuery(objColumnModel, objState);
+		LOG.trace("Invoking query to load current rows: " + strQuery);
+
+		Connection objConn = getConnSource().obtainConnection();
+		Statement objStmt = objConn.createStatement();
+		return objStmt.executeQuery(strQuery);
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.sql.ISqlTableDataSource#closeResultSet(ResultSet)
+	 */
+	public void closeResultSet(ResultSet objResultSet)
+	{
+		try
+		{
+			Statement objStmt = objResultSet.getStatement();
+			Connection objConn = objStmt.getConnection();
+			try
+			{
+				objResultSet.close();
+				objStmt.close();
+			}
+			catch (SQLException e)
+			{
+				// ignore
+			}
+			getConnSource().returnConnection(objConn);
+		}
+		catch (SQLException e)
+		{
+			LOG.warn("Error while closing the result set", e);
+		}
+	}
+
+	protected String quoteObjectName(String strObject)
+	{
+		return strObject;
+	}
+
+	/**
+	 * Returns the tableName.
+	 * @return String
+	 */
+	public String getTableName()
+	{
+		return m_strTableName;
+	}
+
+	/**
+	 * Sets the tableName.
+	 * @param tableName The tableName to set
+	 */
+	public void setTableName(String tableName)
+	{
+		m_strTableName = tableName;
+	}
+
+	/**
+	 * Returns the connSource.
+	 * @return ISqlConnectionSource
+	 */
+	public ISqlConnectionSource getConnSource()
+	{
+		return m_objConnSource;
+	}
+
+	/**
+	 * Sets the connSource.
+	 * @param connSource The connSource to set
+	 */
+	public void setConnSource(ISqlConnectionSource connSource)
+	{
+		m_objConnSource = connSource;
+	}
+
+	/**
+	 * Returns the whereClause.
+	 * @return String
+	 */
+	public String getWhereClause()
+	{
+		return m_strWhereClause;
+	}
+
+	/**
+	 * Sets the whereClause.
+	 * @param whereClause The whereClause to set
+	 */
+	public void setWhereClause(String whereClause)
+	{
+		m_strWhereClause = whereClause;
+	}
+
+	protected String generateColumnList(SqlTableColumnModel objColumnModel)
+	{
+		// build the column selection
+		StringBuffer objColumnBuf = new StringBuffer();
+		for (int i = 0; i < objColumnModel.getColumnCount(); i++)
+		{
+			SqlTableColumn objColumn = objColumnModel.getSqlColumn(i);
+			if (i > 0)
+				objColumnBuf.append(", ");
+			objColumnBuf.append(quoteObjectName(objColumn.getColumnName()));
+		}
+
+		return objColumnBuf.toString();
+	}
+
+	protected String generateWhereClause()
+	{
+		String strWhereClause = getWhereClause();
+		if (strWhereClause == null || strWhereClause.equals(""))
+			return "";
+		return "WHERE " + strWhereClause + " ";
+	}
+
+	protected String generateOrderByClause(ITableSortingState objSortingState)
+	{
+		// build the sorting clause
+		StringBuffer objSortingBuf = new StringBuffer();
+		if (objSortingState.getSortColumn() != null)
+		{
+			objSortingBuf.append("ORDER BY ");
+			objSortingBuf.append(objSortingState.getSortColumn());
+			if (objSortingState.getSortOrder()
+				== ITableSortingState.SORT_ASCENDING)
+				objSortingBuf.append(" ASC ");
+			else
+				objSortingBuf.append(" DESC ");
+		}
+
+		return objSortingBuf.toString();
+	}
+
+	protected String generateLimitClause(ITablePagingState objPagingState)
+	{
+		int nPageSize = objPagingState.getPageSize();
+		int nStart = objPagingState.getCurrentPage() * nPageSize;
+		String strPagingBuf = "LIMIT " + nPageSize + " OFFSET " + nStart + " ";
+		return strPagingBuf;
+	}
+
+	protected String generateDataQuery(
+		SqlTableColumnModel objColumnModel,
+		SimpleTableState objState)
+	{
+		String strQuery =
+			"SELECT "
+				+ generateColumnList(objColumnModel)
+				+ " FROM "
+				+ getTableName()
+				+ " "
+				+ generateWhereClause()
+				+ generateOrderByClause(objState.getSortingState())
+				+ generateLimitClause(objState.getPagingState());
+
+		return strQuery;
+	}
+
+	protected String generateCountQuery()
+	{
+		String strQuery =
+			"SELECT COUNT(*) FROM "
+				+ getTableName()
+				+ " "
+				+ generateWhereClause();
+
+		return strQuery;
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/SqlTableColumn.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/SqlTableColumn.java
new file mode 100644
index 0000000..9698bcb
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/SqlTableColumn.java
@@ -0,0 +1,78 @@
+//  Copyright 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.tapestry.contrib.table.model.sql;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.contrib.table.model.simple.SimpleTableColumn;
+
+/**
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public class SqlTableColumn extends SimpleTableColumn
+{
+	private static final Log LOG = LogFactory.getLog(SqlTableColumn.class);
+
+	/**
+	 * Creates an SqlTableColumn
+	 * @param strSqlField the identifying name of the column and the SQL field it refers to
+	 * @param strDisplayName the display name of the column
+	 */
+	public SqlTableColumn(String strSqlField, String strDisplayName)
+	{
+		super(strSqlField, strDisplayName);
+	}
+
+	/**
+	 * Creates an SqlTableColumn
+	 * @param strSqlField the identifying name of the column and the SQL field it refers to
+	 * @param strDisplayName the display name of the column
+	 * @param bSortable whether the column is sortable
+	 */
+	public SqlTableColumn(
+		String strSqlField,
+		String strDisplayName,
+		boolean bSortable)
+	{
+		super(strSqlField, strDisplayName, bSortable);
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.simple.SimpleTableColumn#getColumnValue(Object)
+	 */
+	public Object getColumnValue(Object objRow)
+	{
+		try
+		{
+			ResultSet objRS = (ResultSet) objRow;
+            String strColumnName = getColumnName();
+			Object objValue = objRS.getObject(strColumnName);
+			if (objValue == null)
+				objValue = "";
+			return objValue;
+		}
+		catch (SQLException e)
+		{
+			LOG.error("Cannot get the value for column: " + getColumnName(), e);
+			return "";
+		}
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/SqlTableColumnModel.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/SqlTableColumnModel.java
new file mode 100644
index 0000000..8fe47e7
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/SqlTableColumnModel.java
@@ -0,0 +1,40 @@
+//  Copyright 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.tapestry.contrib.table.model.sql;
+
+import org.apache.tapestry.contrib.table.model.simple.SimpleTableColumnModel;
+
+/**
+ * 
+ * @version $Id$
+ * @author mindbridge
+ */
+public class SqlTableColumnModel extends SimpleTableColumnModel
+{
+	public SqlTableColumnModel(SqlTableColumn[] arrColumns)
+	{
+		super(arrColumns);
+	}
+
+	public SqlTableColumn getSqlColumn(int nColumn)
+	{
+		return (SqlTableColumn) getColumn(nColumn);
+	}
+
+	public SqlTableColumn getSqlColumn(String strColumn)
+	{
+		return (SqlTableColumn) getColumn(strColumn);
+	}
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/SqlTableModel.java b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/SqlTableModel.java
new file mode 100644
index 0000000..61c3aec
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/table/model/sql/SqlTableModel.java
@@ -0,0 +1,156 @@
+//  Copyright 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.tapestry.contrib.table.model.sql;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Iterator;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.contrib.table.model.ITableColumnModel;
+import org.apache.tapestry.contrib.table.model.common.AbstractTableModel;
+import org.apache.tapestry.contrib.table.model.simple.SimpleTableState;
+
+/**
+ * An implementation of ITableModel that obtains its data through SQL queries.
+ * This is a very efficient model, since it uses SQL to perform
+ * the data sorting (through ORDER BY) and obtains only the data
+ * on the current page (through LIMIT/OFFSET). 
+ * <p>
+ * This object is typically created in the following manner:
+ * <pre> 
+ *    ISqlConnectionSource objConnSrc = 
+ *        new SimpleSqlConnectionSource("jdbc:postgresql://localhost/testdb", "testdb", "testdb");
+ *
+ *    ISqlTableDataSource objDataSrc = 
+ *        new SimpleSqlTableDataSource(objConnSrc, "test_table");
+ *
+ *    SqlTableColumnModel objColumnModel = 
+ *        new SqlTableColumnModel(new SqlTableColumn[] {
+ *            new SqlTableColumn("language", "Language", true),
+ *            new SqlTableColumn("country", "Country", true),
+ *            new SqlTableColumn("variant", "Variant", true),
+ *            new SqlTableColumn("intvalue", "Integer", true),
+ *            new SqlTableColumn("floatvalue", "Float", true)
+ *        });
+ *
+ *    ITableModel objTableModel = new SqlTableModel(objDataSrc, objColumnModel);
+ *
+ *    return objTableModel;
+ * </pre> 
+ *  
+ * @version $Id$
+ * @author mindbridge
+ */
+public class SqlTableModel extends AbstractTableModel 
+{
+	private static final Log LOG = LogFactory.getLog(SqlTableModel.class);
+
+	private ISqlTableDataSource m_objDataSource;
+	private SqlTableColumnModel m_objColumnModel;
+    
+    {
+        try {
+            Class.forName ( "org.hsqldb.jdbcDriver" );
+        } catch (Exception e) {
+            System.out.println("ERROR: failed to load HSQLDB JDBC driver.");
+            e.printStackTrace();
+        }
+    }
+
+	public SqlTableModel(
+		ISqlTableDataSource objDataSource,
+		SqlTableColumnModel objColumnModel)
+	{
+		this(objDataSource, objColumnModel, new SimpleTableState());
+	}
+
+	public SqlTableModel(
+		ISqlTableDataSource objDataSource,
+		SqlTableColumnModel objColumnModel,
+		SimpleTableState objState)
+	{
+        super(objState);
+		m_objDataSource = objDataSource;
+		m_objColumnModel = objColumnModel;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableModel#getColumnModel()
+	 */
+	public ITableColumnModel getColumnModel()
+	{
+		return m_objColumnModel;
+	}
+
+	public SqlTableColumnModel getSqlColumnModel()
+	{
+		return m_objColumnModel;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableModel#getCurrentPageRows()
+	 */
+	public Iterator getCurrentPageRows()
+	{
+		try
+		{
+			ResultSet objResultSet =
+				getSqlDataSource().getCurrentRows(
+					getSqlColumnModel(),
+					getState());
+
+			return new ResultSetIterator(objResultSet)
+			{
+				protected void notifyEnd()
+				{
+					getSqlDataSource().closeResultSet(getResultSet());
+				}
+			};
+		}
+		catch (SQLException e)
+		{
+			LOG.error("Cannot get current page rows", e);
+			return new ResultSetIterator(null);
+		}
+	}
+
+	/**
+	 * Returns the dataSource.
+	 * @return ISqlTableDataSource
+	 */
+	public ISqlTableDataSource getSqlDataSource()
+	{
+		return m_objDataSource;
+	}
+
+    /**
+     * @see org.apache.tapestry.contrib.table.model.common.AbstractTableModel#getRowCount()
+     */
+    protected int getRowCount()
+    {
+        try
+        {
+            return m_objDataSource.getRowCount();
+        }
+        catch (SQLException e)
+        {
+            LOG.error("Cannot get row count", e);
+            return 1;
+        }
+    }
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/INodeRenderFactory.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/INodeRenderFactory.java
new file mode 100644
index 0000000..13f3ea3
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/INodeRenderFactory.java
@@ -0,0 +1,29 @@
+//  Copyright 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.tapestry.contrib.tree.components;
+
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.tree.model.ITreeModelSource;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public interface INodeRenderFactory 
+{
+	IRender getRenderByID(Object objUniqueKey, ITreeModelSource objTreeModelSource, IRequestCycle objCycle);	
+	IRender getRender(Object objValue, ITreeModelSource objTreeModelSource, IRequestCycle objCycle);	
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/ITreeComponent.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/ITreeComponent.java
new file mode 100644
index 0000000..b83fd5a
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/ITreeComponent.java
@@ -0,0 +1,30 @@
+//  Copyright 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.tapestry.contrib.tree.components;
+
+import org.apache.tapestry.contrib.tree.model.ITreeModelSource;
+import org.apache.tapestry.contrib.tree.model.ITreeRowSource;
+import org.apache.tapestry.util.ComponentAddress;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public interface ITreeComponent {
+	ComponentAddress getComponentPath();
+	ITreeModelSource getTreeModelSource();
+	ITreeRowSource getTreeRowSource();
+	void resetState();
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/Tree.html b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/Tree.html
new file mode 100644
index 0000000..863017b
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/Tree.html
@@ -0,0 +1,7 @@
+<span jwcid="$content$">
+	<span class="tree" jwcid="treeView">
+		<span jwcid="treeData">
+			<span jwcid="treeNodeValue"/>
+		</span>
+	</span>		
+</span>		
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/Tree.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/Tree.java
new file mode 100644
index 0000000..c0e54d0
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/Tree.java
@@ -0,0 +1,63 @@
+//  Copyright 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.tapestry.contrib.tree.components;
+
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.contrib.tree.model.ITreeModelSource;
+import org.apache.tapestry.contrib.tree.model.ITreeRowSource;
+import org.apache.tapestry.util.ComponentAddress;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public class Tree extends BaseComponent implements ITreeComponent{
+
+	public Tree() {
+		super();
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.components.ITreeComponent#getComponentPath()
+	 */
+	public ComponentAddress getComponentPath() {
+		return new ComponentAddress(this);
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.components.ITreeComponent#getTreeModelSource()
+	 */
+	public ITreeModelSource getTreeModelSource() {
+		TreeView objTreeView = (TreeView)getComponent("treeView");
+		return objTreeView;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.components.ITreeComponent#resetState()
+	 */
+	public void resetState() {
+		TreeView objTreeView = (TreeView)getComponent("treeView");
+		objTreeView.resetState();
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.components.ITreeComponent#getTreeRowSource()
+	 */
+	public ITreeRowSource getTreeRowSource() {
+		TreeDataView objTreeDataView = (TreeDataView)getComponent("treeData");
+		return objTreeDataView;
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/Tree.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/Tree.jwc
new file mode 100644
index 0000000..d69529a
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/Tree.jwc
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!--  $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.contrib.tree.components.Tree"
+    allow-body="yes" allow-informal-parameters="yes">
+
+    <parameter name="sessionStateManager"
+        type="org.apache.tapestry.contrib.tree.model.ITreeSessionStateManager"
+        direction="custom" required="no"/>
+
+    <parameter name="sessionStoreManager"
+        type="org.apache.tapestry.contrib.tree.model.ISessionStoreManager"
+        direction="custom" required="no"/>
+
+    <parameter name="treeModel"
+        type="org.apache.tapestry.contrib.tree.model.ITreeModel"
+        direction="custom" required="yes">
+    </parameter>
+
+    <parameter name="treeStateListener"
+        type="org.apache.tapestry.contrib.tree.model.ITreeStateListener"
+        direction="custom" required="no">
+    </parameter>
+
+    <parameter name="closeNodeImage" type="org.apache.tapestry.IAsset"
+               required="no" direction="in"/>
+    <parameter name="openNodeImage" type="org.apache.tapestry.IAsset"
+               required="no" direction="in"/>
+
+    <parameter name="showNodeImages" type="boolean" required="no"
+               direction="custom"/>
+    <parameter name="makeNodeDirect" type="boolean" required="no"
+               direction="custom"/>
+    <parameter name="nodeRenderFactory"
+               type="org.apache.tapestry.contrib.tree.components.INodeRenderFactory"
+               required="no" direction="custom"/>
+
+    <component id="treeView" type="TreeView">
+        <inherited-binding name="sessionStateManager" parameter-name="sessionStateManager"/>
+        <inherited-binding name="sessionStoreManager" parameter-name="sessionStoreManager"/>
+        <inherited-binding name="treeModel" parameter-name="treeModel"/>
+        <inherited-binding name="treeStateListener" parameter-name="treeStateListener"/>
+    </component>
+
+    <component id="treeData" type="TreeDataView">
+        <binding name="treeView" expression='components.treeView'/>
+        <!--inherited-binding name="value" parameter-name="value"/-->
+    </component>
+
+    <component id="treeNodeValue" type="TreeNodeView">
+        <inherited-binding name="closeNodeImage" parameter-name="closeNodeImage"/>
+        <inherited-binding name="openNodeImage" parameter-name="openNodeImage"/>
+        <inherited-binding name="showNodeImages" parameter-name="showNodeImages"/>
+        <inherited-binding name="makeNodeDirect" parameter-name="makeNodeDirect"/>
+        <inherited-binding name="nodeRenderFactory" parameter-name="nodeRenderFactory"/>
+
+        <binding name="treeDataView" expression='components.treeData'/>
+    </component>
+
+</component-specification>
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeDataView.html b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeDataView.html
new file mode 100644
index 0000000..39a93c5
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeDataView.html
@@ -0,0 +1,3 @@
+<!-- generated by Spindle, http://spindle.sourceforge.net -->
+
+<span jwcid="$content$"><span jwcid="wrapped"/></span>
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeDataView.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeDataView.java
new file mode 100644
index 0000000..6514d92
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeDataView.java
@@ -0,0 +1,123 @@
+//  Copyright 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.tapestry.contrib.tree.components;
+
+import java.util.Iterator;
+
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.tree.model.ITreeDataModel;
+import org.apache.tapestry.contrib.tree.model.ITreeModel;
+import org.apache.tapestry.contrib.tree.model.ITreeRowSource;
+import org.apache.tapestry.contrib.tree.model.TreeRowObject;
+
+/**
+ * @version $Id$
+ */
+public class TreeDataView extends BaseComponent implements ITreeRowSource{
+    private IBinding m_objTreeViewBinding;
+
+	private TreeRowObject m_objTreeRowObject = null;
+    private int m_nTreeDeep = -1;
+
+    public TreeDataView(){
+        super();
+        initialize();
+    }
+
+    public void initialize(){
+		m_objTreeRowObject = null;
+        m_nTreeDeep = -1;
+    }
+
+    /**
+     * Returns the treeViewBinding.
+     * @return IBinding
+     */
+    public IBinding getTreeViewBinding() {
+        return m_objTreeViewBinding;
+    }
+
+    /**
+     * Sets the treeViewBinding.
+     * @param treeViewBinding The treeViewBinding to set
+     */
+    public void setTreeViewBinding(IBinding treeViewBinding) {
+        m_objTreeViewBinding = treeViewBinding;
+    }
+
+    public TreeView getTreeView() {
+        return (TreeView) m_objTreeViewBinding.getObject();
+    }
+
+    public void renderComponent(IMarkupWriter writer, IRequestCycle cycle) {
+        // render data
+		Object objExistedTreeModelSource = cycle.getAttribute(ITreeRowSource.TREE_ROW_SOURCE_ATTRIBUTE);
+		cycle.setAttribute(ITreeRowSource.TREE_ROW_SOURCE_ATTRIBUTE, this);
+	
+        TreeView objView = getTreeView();
+        ITreeModel objTreeModel = objView.getTreeModel();
+        ITreeDataModel objTreeDataModel = objTreeModel.getTreeDataModel();
+        Object objValue = objTreeDataModel.getRoot();
+        Object objValueUID = objTreeDataModel.getUniqueKey(objValue, null);
+
+        // Object objSelectedNode = objTreeModel.getTreeStateModel().getSelectedNode();
+        //if(objSelectedNode == null)
+        //  objTreeModel.getTreeStateModel().expand(objValueUID);
+
+        walkTree(objValue, objValueUID, 0, objTreeModel, writer, cycle);
+
+		cycle.setAttribute(ITreeRowSource.TREE_ROW_SOURCE_ATTRIBUTE, objExistedTreeModelSource);
+    }
+
+    public void walkTree(Object objParent, Object objParentUID, int nDepth,
+                         ITreeModel objTreeModel, IMarkupWriter writer,
+                         IRequestCycle cycle) {
+		m_objTreeRowObject = new TreeRowObject(objParent, objParentUID, nDepth);
+        m_nTreeDeep = nDepth;
+
+        super.renderComponent(writer, cycle);
+
+        boolean bContain = objTreeModel.getTreeStateModel().isUniqueKeyExpanded(objParentUID);
+        if (bContain) {
+            for (Iterator iter = objTreeModel.getTreeDataModel().getChildren(objParent); iter.hasNext();) {
+                Object objChild = iter.next();
+                Object objChildUID = objTreeModel.getTreeDataModel().getUniqueKey(objChild, objParentUID);
+                walkTree(objChild, objChildUID, nDepth+1, objTreeModel, writer, cycle);
+            }
+        }
+    }
+
+    public int getTreeDeep() {
+        return m_nTreeDeep;
+    }
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeRowSource#getTreeRow()
+	 */
+	public TreeRowObject getTreeRow() {
+		return getTreeRowObject();
+	}
+
+	public TreeRowObject getTreeRowObject() {
+		return m_objTreeRowObject;
+	}
+
+	public void setTreeRowObject(TreeRowObject object) {
+		m_objTreeRowObject = object;
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeDataView.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeDataView.jwc
new file mode 100644
index 0000000..c513d45
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeDataView.jwc
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!--  $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.contrib.tree.components.TreeDataView"
+    allow-body="yes" allow-informal-parameters="yes">
+
+    <parameter name="treeView"
+               type="org.apache.tapestry.contrib.tree.components.TreeView"
+               required="yes" direction="custom"/>
+
+    <parameter name="value" type="java.lang.Object" required="no" direction="custom"/>
+
+    <component id="wrapped" type="RenderBody"/>
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeNodeView.html b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeNodeView.html
new file mode 100644
index 0000000..ab24cde
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeNodeView.html
@@ -0,0 +1,19 @@
+<!-- generated by Spindle, http://spindle.sourceforge.net -->
+
+<span jwcid="$content$">
+	<span jwcid="offset">
+		<span jwcid="makeNodeDirect">
+			<a jwcid="direct">
+				<span jwcid="showImages">
+					<img jwcid="imageNode"/>
+				</span>
+				<span jwcid="insertValue"/></a><br>
+		</span>
+		<span jwcid="makeNodeNoDirect">
+			<span jwcid="showImages2">
+				<img jwcid="imageNode2"/>
+			</span>
+			<span jwcid="insertValue2"/><br>
+		</span>
+	</span>
+</span>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeNodeView.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeNodeView.java
new file mode 100644
index 0000000..2db1d7f
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeNodeView.java
@@ -0,0 +1,399 @@
+//  Copyright 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.tapestry.contrib.tree.components;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.IAsset;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.tree.model.ITreeModelSource;
+import org.apache.tapestry.contrib.tree.model.ITreeRowSource;
+import org.apache.tapestry.contrib.tree.model.ITreeStateListener;
+import org.apache.tapestry.contrib.tree.model.ITreeStateModel;
+import org.apache.tapestry.contrib.tree.model.TreeRowObject;
+import org.apache.tapestry.contrib.tree.model.TreeStateEvent;
+import org.apache.tapestry.contrib.tree.simple.SimpleNodeRenderFactory;
+import org.apache.tapestry.engine.IPageLoader;
+import org.apache.tapestry.event.PageDetachListener;
+import org.apache.tapestry.event.PageEvent;
+import org.apache.tapestry.spec.ComponentSpecification;
+import org.apache.tapestry.util.ComponentAddress;
+
+/**
+ * @version $Id$
+ **/
+public class TreeNodeView extends BaseComponent implements PageDetachListener{
+    private static final Log LOG = LogFactory.getLog(TreeNodeView.class);
+
+    private IBinding m_objNodeRenderFactoryBinding;
+    private IBinding m_objShowNodeImagesBinding;
+    private IBinding m_objMakeNodeDirectBinding;
+    private Boolean m_objNodeState;
+    private Boolean m_objShowNodeImages;
+    private Boolean m_objMakeNodeDirect;
+    private INodeRenderFactory m_objNodeRenderFactory;
+
+    private IAsset m_objOpenNodeImage;
+    private IAsset m_objCloseNodeImage;
+
+    public TreeNodeView(){
+        super();
+        initialize();
+    }
+
+    private void initialize(){
+        m_objNodeState = null;
+        m_objShowNodeImages = null;
+        m_objNodeRenderFactory = null;
+        m_objMakeNodeDirect = null;
+    }
+
+    public IRender getCurrentRenderer(){
+        INodeRenderFactory objRenderFactory = getNodeRenderFactory();
+		ITreeRowSource objTreeRowSource = getTreeRowSource();
+        return objRenderFactory.getRender(objTreeRowSource.getTreeRow().getTreeNode(),
+                                          getTreeModelSource(),
+                                          getPage().getRequestCycle());
+    }
+
+    public Object[] getNodeContext(){
+		ITreeModelSource objModelSource = getTreeModelSource();
+		ComponentAddress objModelSourceAddress = new ComponentAddress(objModelSource);
+		ITreeRowSource objTreeRowSource = getTreeRowSource();
+		TreeRowObject objTreeRowObject = objTreeRowSource.getTreeRow();
+        Object objValueUID = objTreeRowObject.getTreeNodeUID();
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("getNodeContext objValueUID = " + objValueUID);
+        }
+
+        return new Object[] { objValueUID, new Boolean(isNodeOpen()), objModelSourceAddress };
+    }
+
+    /**
+     * Called when a node in the tree is clicked by the user.
+     * If the node is expanded, it will be collapsed, and vice-versa,
+     * that is, the tree state model is retrieved, and it is told
+     * to collapse or expand the node.
+     *
+     * @param cycle The Tapestry request cycle object.
+     */
+    public void nodeSelect(IRequestCycle cycle) {
+        Object context[] = cycle.getServiceParameters();
+        Object objValueUID = null;
+        if (context != null && context.length > 0) {
+            objValueUID = context[0];
+        }
+		ComponentAddress objModelSourceAddress = (ComponentAddress)context[2];
+		ITreeModelSource objTreeModelSource = (ITreeModelSource) objModelSourceAddress.findComponent(cycle);
+		//ITreeModelSource objTreeModelSource = getTreeModelSource();
+        ITreeStateModel objStateModel = objTreeModelSource.getTreeModel().getTreeStateModel();
+        boolean bState = objStateModel.isUniqueKeyExpanded(objValueUID);
+
+        if (bState) {
+            objStateModel.collapse(objValueUID);
+            fireNodeCollapsed(objValueUID, objTreeModelSource);
+        } else {
+            objStateModel.expandPath(objValueUID);
+			fireNodeExpanded(objValueUID, objTreeModelSource);
+        }
+    }
+
+	private void fireNodeCollapsed(Object objValueUID, ITreeModelSource objTreeModelSource){
+		deliverEvent(TreeStateEvent.NODE_COLLAPSED, objValueUID, objTreeModelSource);
+	
+	}
+
+	private void fireNodeExpanded(Object objValueUID, ITreeModelSource objTreeModelSource){
+		deliverEvent(TreeStateEvent.NODE_EXPANDED, objValueUID, objTreeModelSource);
+	}
+    
+	private void deliverEvent(int nEventUID, Object objValueUID, ITreeModelSource objTreeModelSource){
+		ITreeStateListener objListener = objTreeModelSource.getTreeStateListener();
+		if(objListener != null){
+			TreeStateEvent objEvent = new TreeStateEvent(nEventUID, objValueUID, objTreeModelSource.getTreeModel().getTreeStateModel());
+			objListener.treeStateChanged(objEvent); 
+		}
+		
+	}
+    
+	private void deliverEventOld(int nEventUID, Object objValueUID, ITreeStateModel objStateModel){
+		IBinding objBinding = getBinding("treeStateListener");
+		if(objBinding != null){
+			ITreeStateListener objListener = (ITreeStateListener)objBinding.getObject();
+			TreeStateEvent objEvent = new TreeStateEvent(nEventUID, objValueUID, objStateModel);
+			objListener.treeStateChanged(objEvent); 
+		}
+		
+	}
+
+    public void pageDetached(PageEvent arg0) {
+        initialize();
+    }
+
+    public void finishLoad(IRequestCycle objCycle, IPageLoader arg0, ComponentSpecification arg1)
+    {
+        super.finishLoad(objCycle, arg0, arg1);
+        getPage().addPageDetachListener(this);
+
+        m_objOpenNodeImage = getAsset("_openNodeImage");
+        m_objCloseNodeImage = getAsset("_closeNodeImage");
+    }
+
+    public boolean isNodeOpen() {
+        if(m_objNodeState == null){
+			ITreeRowSource objTreeRowSource = getTreeRowSource();
+			TreeRowObject objTreeRowObject = objTreeRowSource.getTreeRow();
+            Object objValueUID = objTreeRowObject.getTreeNodeUID();
+			ITreeModelSource objTreeModelSource = getTreeModelSource();
+            ITreeStateModel objStateModel = objTreeModelSource.getTreeModel().getTreeStateModel();
+            boolean bState = objStateModel.isUniqueKeyExpanded(objValueUID);
+            m_objNodeState = new Boolean(bState);
+        }
+        return m_objNodeState.booleanValue();
+    }
+
+    /**
+     * Returns the openNodeImage.
+     * @return IAsset
+     */
+    public IAsset getNodeImage() {
+        if(isNodeOpen()) {
+            if (m_objOpenNodeImage == null) {
+                m_objOpenNodeImage = getAsset("_openNodeImage");
+            }
+            return m_objOpenNodeImage;
+        } else {
+            if (m_objCloseNodeImage == null) {
+                m_objCloseNodeImage = getAsset("_closeNodeImage");
+            }
+            return m_objCloseNodeImage;
+        }
+    }
+
+    /**
+     * Returns the closeNodeImage.
+     * @return IAsset
+     */
+    public IAsset getCloseNodeImage() {
+        return m_objCloseNodeImage;
+    }
+
+    /**
+     * Returns the openNodeImage.
+     * @return IAsset
+     */
+    public IAsset getOpenNodeImage() {
+        return m_objOpenNodeImage;
+    }
+
+    /**
+     * Sets the closeNodeImage.
+     * @param closeNodeImage The closeNodeImage to set
+     */
+    public void setCloseNodeImage(IAsset closeNodeImage) {
+        m_objCloseNodeImage = closeNodeImage;
+    }
+
+    /**
+     * Sets the openNodeImage.
+     * @param openNodeImage The openNodeImage to set
+     */
+    public void setOpenNodeImage(IAsset openNodeImage) {
+        m_objOpenNodeImage = openNodeImage;
+    }
+
+    /**
+     * @see
+     * org.apache.tapestry.AbstractComponent#renderComponent(IMarkupWriter,
+     * IRequestCycle)
+     */
+    protected void renderComponent(IMarkupWriter arg0, IRequestCycle arg1) {
+        super.renderComponent(arg0, arg1);
+        m_objNodeState = null;
+    }
+
+    /**
+     * Returns the ShowNodeImagesBinding.
+     * @return IBinding
+     */
+    public IBinding getShowNodeImagesBinding() {
+        return m_objShowNodeImagesBinding;
+    }
+
+    /**
+     * Sets the ShowNodeImagesBinding.
+     * @param ShowNodeImagesBinding The ShowNodeImagesBinding to set
+     */
+    public void setShowNodeImagesBinding(IBinding ShowNodeImagesBinding) {
+        m_objShowNodeImagesBinding = ShowNodeImagesBinding;
+    }
+
+    /**
+     * Returns the ShowNodeImages.
+     * @return Boolean
+     */
+    public Boolean isShowNodeImages() {
+        if(m_objShowNodeImages == null){
+            if(getNodeRenderFactoryBinding() == null){
+                m_objShowNodeImages = Boolean.TRUE;
+            } else {
+                if(m_objShowNodeImagesBinding != null) {
+                    m_objShowNodeImages = (Boolean)
+                        m_objShowNodeImagesBinding.getObject();
+                } else {
+                    m_objShowNodeImages = Boolean.TRUE;
+                }
+            }
+        }
+        return m_objShowNodeImages;
+    }
+
+    public boolean getShowImages(){
+        boolean bResult = isShowNodeImages().booleanValue();
+        return bResult;
+    }
+
+    public boolean getShowWithoutImages(){
+        boolean bResult = !isShowNodeImages().booleanValue();
+        return bResult;
+    }
+
+    public String getOffsetStyle() {
+        //return "width: " + getTreeDataView().getTreeDeep() * 15;
+		ITreeRowSource objTreeRowSource = getTreeRowSource();
+		TreeRowObject objTreeRowObject = objTreeRowSource.getTreeRow();
+        int nTreeRowDepth = 0;
+        if(objTreeRowObject != null){
+			nTreeRowDepth = objTreeRowObject.getTreeRowDepth();
+        }
+        return "padding-left: " + nTreeRowDepth * 15+"px";
+    }
+
+    /**
+     * Returns the nodeRenderFactoryBinding.
+     * @return IBinding
+     */
+    public IBinding getNodeRenderFactoryBinding() {
+        return m_objNodeRenderFactoryBinding;
+    }
+
+    /**
+     * Sets the nodeRenderFactoryBinding.
+     * @param nodeRenderFactoryBinding The nodeRenderFactoryBinding to set
+     */
+    public void setNodeRenderFactoryBinding(IBinding nodeRenderFactoryBinding) {
+        m_objNodeRenderFactoryBinding = nodeRenderFactoryBinding;
+    }
+
+    public INodeRenderFactory getNodeRenderFactory() {
+        if(m_objNodeRenderFactory == null){
+            IBinding objBinding = getNodeRenderFactoryBinding();
+            if( objBinding != null){
+                m_objNodeRenderFactory = (INodeRenderFactory)objBinding.getObject();
+            }else{
+                m_objNodeRenderFactory = new SimpleNodeRenderFactory();
+            }
+        }
+        return m_objNodeRenderFactory;
+    }
+
+    /**
+     * Returns the makeNodeDirectBinding.
+     * @return IBinding
+     */
+    public IBinding getMakeNodeDirectBinding() {
+        return m_objMakeNodeDirectBinding;
+    }
+
+    /**
+     * Sets the makeNodeDirectBinding.
+     * @param makeNodeDirectBinding The makeNodeDirectBinding to set
+     */
+    public void setMakeNodeDirectBinding(IBinding makeNodeDirectBinding) {
+        m_objMakeNodeDirectBinding = makeNodeDirectBinding;
+    }
+
+    /**
+     * Returns the makeNodeDirect.
+     * @return Boolean
+     */
+    public boolean getMakeNodeDirect() {
+        if(m_objMakeNodeDirect == null){
+            IBinding objBinding = getMakeNodeDirectBinding();
+            if(objBinding != null){
+                m_objMakeNodeDirect = (Boolean)objBinding.getObject();
+            }else{
+                m_objMakeNodeDirect = Boolean.TRUE;
+            }
+        }
+        return m_objMakeNodeDirect.booleanValue();
+    }
+    public boolean getMakeNodeNoDirect() {
+        return !getMakeNodeDirect();
+    }
+
+
+    public String getCleanSelectedID(){
+        return getSelectedNodeID();
+    }
+
+    public String getSelectedID(){
+		ITreeRowSource objTreeRowSource = getTreeRowSource();
+		ITreeModelSource objTreeModelSource = getTreeModelSource();
+		TreeRowObject objTreeRowObject = objTreeRowSource.getTreeRow();
+        Object objNodeValueUID = objTreeRowObject.getTreeNodeUID();
+        Object objSelectedNode = objTreeModelSource.getTreeModel().getTreeStateModel().getSelectedNode();
+        if(objNodeValueUID.equals(objSelectedNode)) {
+            return getSelectedNodeID();
+        }
+        return "";
+    }
+		
+	private String getSelectedNodeID(){
+		//return getTreeDataView().getTreeView().getSelectedNodeID();
+		return "tree";	
+	}
+		
+    public String getNodeStyleClass() {
+		ITreeRowSource objTreeRowSource = getTreeRowSource();
+		ITreeModelSource objTreeModelSource = getTreeModelSource();
+		TreeRowObject objTreeRowObject = objTreeRowSource.getTreeRow();
+		boolean bResult = false;
+		if(objTreeRowObject != null){
+	        Object objNodeValueUID = objTreeRowObject.getTreeNodeUID();
+	        Object objSelectedNode = objTreeModelSource.getTreeModel().getTreeStateModel().getSelectedNode();
+			bResult = objNodeValueUID.equals(objSelectedNode);
+		}
+        if (bResult) {
+            return "selectedNodeViewClass";
+        }
+
+        return "notSelectedNodeViewClass";
+    }
+    
+    public ITreeRowSource getTreeRowSource(){
+		ITreeRowSource objSource = (ITreeRowSource)getPage().getRequestCycle().getAttribute(ITreeRowSource.TREE_ROW_SOURCE_ATTRIBUTE);
+    	return objSource;
+    }
+
+	public ITreeModelSource getTreeModelSource(){
+		ITreeModelSource objSource = (ITreeModelSource)getPage().getRequestCycle().getAttribute(ITreeModelSource.TREE_MODEL_SOURCE_ATTRIBUTE);
+		return objSource;
+	}
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeNodeView.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeNodeView.jwc
new file mode 100644
index 0000000..9adadce
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeNodeView.jwc
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!--  $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification
+    class="org.apache.tapestry.contrib.tree.components.TreeNodeView"
+    allow-body="yes" allow-informal-parameters="yes">
+
+    <parameter name="closeNodeImage" type="org.apache.tapestry.IAsset"
+               required="no" direction="in"/>
+    <parameter name="openNodeImage" type="org.apache.tapestry.IAsset"
+               required="no" direction="in"/>
+
+    <parameter name="showNodeImages" type="boolean" required="no"
+               direction="custom"/>
+    <parameter name="makeNodeDirect" type="boolean" required="no"
+               direction="custom"/>
+
+    <parameter name="nodeRenderFactory"
+               type="org.apache.tapestry.contrib.tree.components.INodeRenderFactory"
+               required="no" direction="custom"/>
+
+    <!--parameter name="treeStateListener"
+        type="org.apache.tapestry.contrib.tree.model.ITreeStateListener"
+        direction="custom" required="no">
+    </parameter-->
+
+    <reserved-parameter name="opennodeimage"/>
+    <reserved-parameter name="treedataview"/>
+    <reserved-parameter name="closenodeimage"/>
+    <reserved-parameter name="nodeviewdirect"/>
+
+    <component id="direct" type="DirectLink">
+        <binding name="parameters" expression="nodeContext"/>
+        <binding name="listener" expression="listeners.nodeSelect"/>
+        <binding name="stateful" expression="false"/>
+        <binding name="name" expression="selectedID"/>
+        <binding name="anchor" expression="cleanSelectedID"/>
+    </component>
+
+    <component id="showImages" type="Conditional">
+        <binding name="condition" expression="showImages"/>
+    </component>
+
+    <component id="showImages2" copy-of="showImages"/>
+
+    <!--component id="showWithoutImages" type="Conditional">
+        <binding name="condition" expression="showWithoutImages"/>
+    </component-->
+
+    <component id="makeNodeDirect" type="Conditional">
+        <binding name="condition" expression="makeNodeDirect"/>
+    </component>
+
+    <component id="makeNodeNoDirect" type="Conditional">
+        <binding name="condition" expression="makeNodeNoDirect"/>
+    </component>
+
+    <component id="imageNode" type="Image">
+        <binding name="image" expression="nodeImage"/>
+    </component>
+    <component id="imageNode2" copy-of="imageNode"/>
+
+    <component id="insertValue" type="Delegator">
+		<binding name="delegate" expression="currentRenderer"/>
+    </component>
+    <component id="insertValue2" copy-of="insertValue"/>
+
+    <component id="offset" type="Any">
+        <static-binding name="element">span</static-binding>
+        <binding name="style" expression="offsetStyle"/>
+        <binding name="class" expression="nodeStyleClass"/>
+    </component>
+
+    <private-asset name="_closeNodeImage" resource-path="plus.gif"/>
+    <private-asset name="_openNodeImage" resource-path="minus.gif"/>
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeNodeViewPage.html b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeNodeViewPage.html
new file mode 100644
index 0000000..6b11e7a
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeNodeViewPage.html
@@ -0,0 +1,5 @@
+<!-- generated by Spindle, http://spindle.sourceforge.net -->
+
+<span jwcid="$content$">
+	<span jwcid="treeNodeView"/>
+</span>
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeNodeViewPage.page b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeNodeViewPage.page
new file mode 100644
index 0000000..eaf27f7
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeNodeViewPage.page
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<!--
+    Copyright 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.
+-->
+<!-- Forms.page,v 1.1 2002/08/23 22:18:31 hship Exp -->
+<!DOCTYPE page-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<page-specification>
+
+    <component id="treeNodeView" type="TreeNodeView">
+    </component>
+
+</page-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeView.html b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeView.html
new file mode 100644
index 0000000..5facb58
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeView.html
@@ -0,0 +1,5 @@
+<span jwcid="$content$">
+    <span jwcid="inheritInformalAny">
+        <span jwcid="insertWrapped"/>
+    </span>
+</span>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeView.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeView.java
new file mode 100644
index 0000000..b926226
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeView.java
@@ -0,0 +1,324 @@
+//  Copyright 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.tapestry.contrib.tree.components;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.tree.model.ISessionStoreManager;
+import org.apache.tapestry.contrib.tree.model.ITreeModel;
+import org.apache.tapestry.contrib.tree.model.ITreeModelSource;
+import org.apache.tapestry.contrib.tree.model.ITreeSessionStateManager;
+import org.apache.tapestry.contrib.tree.model.ITreeStateListener;
+import org.apache.tapestry.contrib.tree.simple.FullTreeSessionStateManager;
+import org.apache.tapestry.event.PageDetachListener;
+import org.apache.tapestry.event.PageEvent;
+import org.apache.tapestry.event.PageRenderListener;
+import org.apache.tapestry.util.ComponentAddress;
+
+/**
+ * @version $Id$
+ */
+public class TreeView extends BaseComponent
+    implements PageDetachListener, PageRenderListener, ITreeModelSource {
+
+    private static final Log LOG = LogFactory.getLog(TreeView.class);
+
+    private IBinding m_objSessionStoreManagerBinding;
+    private IBinding m_objTreeModelBinding;
+    private IBinding m_objSessionStateManagerBinding;
+
+    private ITreeModel m_objTreeModel;
+    private ITreeSessionStateManager m_objTreeSessionStateManager;
+    private ISessionStoreManager m_objSessionStoreManager;
+    private Object m_objTreeSessionState;
+    private ComponentAddress m_objComponentAddress;
+
+    public TreeView(){
+        super();
+        initialize();
+    }
+
+    private void initialize(){
+        m_objTreeModel = null;
+        m_objTreeSessionStateManager = null;
+        m_objSessionStoreManager = null;
+        m_objTreeSessionState = null;
+        m_objComponentAddress = null;
+    }
+
+    /**
+     * @see org.apache.tapestry.AbstractComponent#finishLoad()
+     */
+    protected void finishLoad() {
+        super.finishLoad();
+        getPage().addPageRenderListener(this);
+        getPage().addPageDetachListener(this);
+    }
+
+    /**
+     * @see org.apache.tapestry.AbstractComponent#renderComponent(IMarkupWriter, IRequestCycle)
+     */
+    /*	protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+        throws RequestCycleException
+	{
+    	renderWrapped(writer, cycle);
+	}
+    */
+
+    /**
+     * @see org.apache.tapestry.event.PageDetachListener#pageDetached(PageEvent)
+     */
+    public void pageDetached(PageEvent arg0) {
+        initialize();
+    }
+
+    /**
+     * @see org.apache.tapestry.event.PageRenderListener#pageBeginRender(PageEvent)
+     */
+    public void pageBeginRender(PageEvent arg0) {
+        if(arg0.getRequestCycle().isRewinding()) {
+            return;
+        }
+        storeSesion();
+    }
+
+    /**
+     * @see org.apache.tapestry.event.PageRenderListener#pageEndRender(PageEvent)
+     */
+    public void pageEndRender(PageEvent arg0) {
+    }
+
+    /**
+     * Returns the treeModelBinding.
+     * @return IBinding
+     */
+    public IBinding getTreeModelBinding() {
+        return m_objTreeModelBinding;
+    }
+
+    /**
+     * Sets the treeModelBinding.
+     * @param treeModelBinding The treeModelBinding to set
+     */
+    public void setTreeModelBinding(IBinding treeModelBinding) {
+        m_objTreeModelBinding = treeModelBinding;
+    }
+
+    /**
+     * Returns the SessionStoreManagerBinding.
+     * @return IBinding
+     */
+    public IBinding getSessionStoreManagerBinding() {
+        return m_objSessionStoreManagerBinding;
+    }
+
+    /**
+     * Returns the sessionStateManagerBinding.
+     * @return IBinding
+     */
+    public IBinding getSessionStateManagerBinding() {
+        return m_objSessionStateManagerBinding;
+    }
+
+    /**
+     * Sets the SessionStoreManagerBinding.
+     * @param sessionStoreManagerBinding The SessionStoreManagerBinding to set
+     */
+    public void setSessionStoreManagerBinding(IBinding
+                                              sessionStoreManagerBinding) {
+        m_objSessionStoreManagerBinding = sessionStoreManagerBinding;
+    }
+
+    /**
+     * Sets the sessionStateManagerBinding.
+     * @param sessionStateManagerBinding The sessionStateManagerBinding to set
+     */
+    public void setSessionStateManagerBinding(IBinding
+                                              sessionStateManagerBinding) {
+        m_objSessionStateManagerBinding = sessionStateManagerBinding;
+    }
+
+    private void extractTreeModel(){
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("TreeView.extractTreeModel()");
+        }
+
+        ISessionStoreManager objHolder = getSessionStoreManager();
+        ITreeSessionStateManager objSessionManager = getTreeSessionStateMgr();
+        Object objSessionState;
+        if (objHolder == null) {
+            objSessionState = getTreeSessionState();
+        } else {
+            objSessionState = objHolder.getSessionState(this.getPage(),
+                                                        "treeSessionState");
+        }
+
+        if (objSessionState != null) {
+            m_objTreeModel = objSessionManager.getModel(objSessionState);
+        } else {
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("TreeView.extractTreeModel() from BINDING");
+            }
+
+            m_objTreeModel = (ITreeModel)getTreeModelBinding().getObject();
+        }
+
+    }
+
+    private void storeSesion(){
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("TreeView.storeSesion()");
+        }
+
+        ITreeSessionStateManager objSessionManager = getTreeSessionStateMgr();
+        Object objSessionState =
+            objSessionManager.getSessionState(getTreeModel());
+
+        store(objSessionState);
+    }
+
+    private void store(Object objSessionState){
+        ISessionStoreManager objHolder = getSessionStoreManager();
+
+        if (objHolder == null) {
+            fireObservedChange("treeSessionState", objSessionState);
+        } else {
+            //String strPath = "treeSessionState";
+            String strPath = getExtendedId();
+            if (LOG.isDebugEnabled())
+                LOG.debug("store(): setting state with: " + strPath);
+            objHolder.setSessionState(this.getPage(), strPath,
+                                      objSessionState);
+        }
+    }
+
+    /**
+     * @see ITreeComponent#resetState()
+     */
+    public void resetState() {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("TreeView.resetState()");
+        }
+        initialize();
+        store(null);
+    }
+
+    /**
+     * Returns the SessionStoreManager.
+     * @return ISessionStoreManager
+     */
+    public ISessionStoreManager getSessionStoreManager() {
+        if (m_objSessionStoreManager == null
+            && getSessionStoreManagerBinding() != null) {
+            m_objSessionStoreManager =
+                (ISessionStoreManager)getSessionStoreManagerBinding().getObject();
+        }
+
+        return m_objSessionStoreManager;
+    }
+
+    /**
+     * Returns the wizardSessionStateMgr.
+     * @return IWizardSessionStateManager
+     */
+    public ITreeSessionStateManager getTreeSessionStateMgr() {
+        if (m_objTreeSessionStateManager == null) {
+            IBinding objBinding = getSessionStateManagerBinding();
+            if(objBinding != null){
+                Object objManager = objBinding.getObject();
+                m_objTreeSessionStateManager =
+                    (ITreeSessionStateManager) objManager;
+            } else {
+                m_objTreeSessionStateManager =
+                    new FullTreeSessionStateManager();
+            }
+        }
+        return m_objTreeSessionStateManager;
+    }
+
+    public ComponentAddress getComponentPath() {
+        if (m_objComponentAddress == null) {
+            m_objComponentAddress = new ComponentAddress(this);
+        }
+        return m_objComponentAddress;
+    }
+
+    /**
+     * Returns the treeModel.
+     * @return ITreeModel
+     */
+    public ITreeModel getTreeModel() {
+        if (m_objTreeModel == null) {
+            extractTreeModel();
+        }
+        return m_objTreeModel;
+    }
+
+    /**
+     * Sets the treeModel.
+     * @param treeModel The treeModel to set
+     */
+    public void setTreeModel(ITreeModel treeModel) {
+        m_objTreeModel = treeModel;
+    }
+
+    /**
+     * Returns the treeSessionState.
+     * @return Object
+     */
+    public Object getTreeSessionState() {
+        return m_objTreeSessionState;
+    }
+
+    /**
+     * Sets the treeSessionState.
+     * @param treeSessionState The treeSessionState to set
+     */
+    public void setTreeSessionState(Object treeSessionState) {
+        m_objTreeSessionState = treeSessionState;
+    }
+
+    public String getSelectedNodeStyleID(){
+        return getId() + ":selected";
+    }
+    
+    /**
+	 * @see org.apache.tapestry.BaseComponent#renderComponent(org.apache.tapestry.IMarkupWriter, org.apache.tapestry.IRequestCycle)
+	 */
+	protected void renderComponent(IMarkupWriter arg0, IRequestCycle arg1) {
+		Object objExistedTreeModelSource = arg1.getAttribute(ITreeModelSource.TREE_MODEL_SOURCE_ATTRIBUTE);
+		arg1.setAttribute(ITreeModelSource.TREE_MODEL_SOURCE_ATTRIBUTE, this);
+		
+		super.renderComponent(arg0, arg1);
+		arg1.setAttribute(ITreeModelSource.TREE_MODEL_SOURCE_ATTRIBUTE, objExistedTreeModelSource);
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeModelSource#getTreeStateListener()
+	 */
+	public ITreeStateListener getTreeStateListener() {
+		ITreeStateListener objListener = null;
+		IBinding objBinding = getBinding("treeStateListener");
+		if(objBinding != null){
+			objListener = (ITreeStateListener) objBinding.getObject();
+		}
+		return objListener;
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeView.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeView.jwc
new file mode 100644
index 0000000..e7f8b1e
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeView.jwc
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!--  $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.contrib.tree.components.TreeView"
+    allow-body="yes" allow-informal-parameters="yes">
+
+    <parameter name="sessionStateManager"
+        type="org.apache.tapestry.contrib.tree.model.ITreeSessionStateManager"
+        direction="custom" required="no"/>
+
+    <parameter name="sessionStoreManager"
+        type="org.apache.tapestry.contrib.tree.model.ISessionStoreManager"
+        direction="custom" required="no"/>
+
+    <parameter name="treeModel"
+        type="org.apache.tapestry.contrib.tree.model.ITreeModel"
+        direction="custom" required="yes">
+    </parameter>
+
+    <parameter name="treeStateListener"
+        type="org.apache.tapestry.contrib.tree.model.ITreeStateListener"
+        direction="custom" required="no">
+    </parameter>
+
+    <component id="inheritInformalAny" type="InheritInformalAny">
+        <static-binding name="element">span</static-binding>
+    </component>
+
+    <component id="insertWrapped" type="RenderBody"/>
+
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeView.script b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeView.script
new file mode 100644
index 0000000..2162e74
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/TreeView.script
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE script PUBLIC "-//Howard Ship//Tapestry Script 1.1//EN"
+  "http://tapestry.sf.net/dtd/Script_1_1.dtd">
+
+<script>
+ <body>
+	function select(){
+		var objNode = document.getElementById("<insert property-path="selectedNodeID"/>");
+		window.scrollTo(0, objNode.y);
+	}
+		
+ </body>
+	<initialization>
+		select();
+	</initialization>
+</script>
diff --git a/examples/Workbench/context/images/minus.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/minus.gif
similarity index 100%
copy from examples/Workbench/context/images/minus.gif
copy to tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/minus.gif
Binary files differ
diff --git a/examples/Workbench/context/images/plus.gif b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/plus.gif
similarity index 100%
copy from examples/Workbench/context/images/plus.gif
copy to tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/plus.gif
Binary files differ
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTable.html b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTable.html
new file mode 100644
index 0000000..60f27d6
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTable.html
@@ -0,0 +1,5 @@
+<span jwcid="$content$">
+	<span class="tree" jwcid="treeView">
+		<span jwcid="treeTableDataView"/>
+	</span>		
+</span>		
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTable.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTable.java
new file mode 100644
index 0000000..c6176d6
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTable.java
@@ -0,0 +1,63 @@
+//  Copyright 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.tapestry.contrib.tree.components.table;
+
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.contrib.tree.components.ITreeComponent;
+import org.apache.tapestry.contrib.tree.components.TreeView;
+import org.apache.tapestry.contrib.tree.model.ITreeModelSource;
+import org.apache.tapestry.contrib.tree.model.ITreeRowSource;
+import org.apache.tapestry.util.ComponentAddress;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public class TreeTable extends BaseComponent implements ITreeComponent{
+
+	/**
+	 * 
+	 */
+	public TreeTable() {
+		super();
+	}
+
+	public ITreeModelSource getTreeModelSource(){
+		return (ITreeModelSource) getComponent("treeView");
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.components.ITreeComponent#resetState()
+	 */
+	public void resetState() {
+		TreeView objTreeView = (TreeView)getComponent("treeView");
+		objTreeView.resetState();
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.components.ITreeComponent#getComponentPath()
+	 */
+	public ComponentAddress getComponentPath() {
+		return new ComponentAddress(this);
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.components.ITreeComponent#getTreeRowSource()
+	 */
+	public ITreeRowSource getTreeRowSource() {
+		TreeTableDataView objTreeDataView = (TreeTableDataView)getComponent("treeTableDataView");
+		return objTreeDataView;
+	}
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTable.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTable.jwc
new file mode 100644
index 0000000..08b4285
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTable.jwc
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!--  $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.contrib.tree.components.table.TreeTable"
+    allow-body="yes" allow-informal-parameters="yes">
+
+    <parameter name="sessionStateManager"
+        type="org.apache.tapestry.contrib.tree.model.ITreeSessionStateManager"
+        direction="custom" required="no"/>
+
+    <parameter name="sessionStoreManager"
+        type="org.apache.tapestry.contrib.tree.model.ISessionStoreManager"
+        direction="custom" required="no"/>
+
+    <parameter name="treeModel"
+        type="org.apache.tapestry.contrib.tree.model.ITreeModel"
+        direction="custom" required="yes">
+    </parameter>
+
+    <parameter name="treeStateListener"
+        type="org.apache.tapestry.contrib.tree.model.ITreeStateListener"
+        direction="custom" required="no">
+    </parameter>
+
+	<parameter name="entriesPerTablePage" 
+		type="int" 
+		required="no"
+		direction="custom"/>
+
+    <parameter name="nodeViewComponentAddress"
+               type="org.apache.tapestry.util.ComponentAddress"
+               required="no" direction="custom"/>
+
+    <parameter name="tableColunms"
+               type="java.util.ArrayList"
+               required="no" direction="custom"/>
+
+    <component id="treeView" type="TreeView">
+        <inherited-binding name="sessionStateManager" parameter-name="sessionStateManager"/>
+        <inherited-binding name="sessionStoreManager" parameter-name="sessionStoreManager"/>
+        <inherited-binding name="treeModel" parameter-name="treeModel"/>
+        <inherited-binding name="treeStateListener" parameter-name="treeStateListener"/>
+    </component>
+
+    <component id="treeTableDataView" type="TreeTableDataView">
+        <binding name="treeView" expression='components.treeView'/>
+        <inherited-binding name="nodeViewComponentAddress" parameter-name="nodeViewComponentAddress"/>
+        <inherited-binding name="entriesPerTablePage" parameter-name="entriesPerTablePage"/>
+        <inherited-binding name="tableColunms" parameter-name="tableColunms"/>
+    </component>
+
+</component-specification>
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableColumn.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableColumn.java
new file mode 100644
index 0000000..bc90eec
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableColumn.java
@@ -0,0 +1,45 @@
+//  Copyright 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.tapestry.contrib.tree.components.table;
+
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.table.model.ITableModelSource;
+import org.apache.tapestry.contrib.table.model.simple.SimpleTableColumn;
+import org.apache.tapestry.util.ComponentAddress;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public class TreeTableColumn extends SimpleTableColumn {
+
+	/**
+	 * @param arg0
+	 * @param arg1
+	 */
+	public TreeTableColumn(String arg0, boolean arg1, ComponentAddress objComponentAddress) {
+		super(arg0, arg1);
+		setValueRendererSource(new TreeTableValueRenderSource(objComponentAddress));
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.common.AbstractTableColumn#getValueRenderer(org.apache.tapestry.IRequestCycle, org.apache.tapestry.contrib.table.model.ITableModelSource, java.lang.Object)
+	 */
+	public IRender getValueRenderer(IRequestCycle arg0, ITableModelSource arg1, Object arg2) {
+		return super.getValueRenderer(arg0, arg1, arg2);
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableDataView.html b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableDataView.html
new file mode 100644
index 0000000..5efd7af
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableDataView.html
@@ -0,0 +1,5 @@
+<!-- generated by Spindle, http://spindle.sourceforge.net -->
+
+<span jwcid="$content$">
+	<span jwcid="table"/>
+</span>
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableDataView.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableDataView.java
new file mode 100644
index 0000000..2c93ffd
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableDataView.java
@@ -0,0 +1,230 @@
+//  Copyright 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.tapestry.contrib.tree.components.table;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+import org.apache.tapestry.contrib.table.model.ITableModel;
+import org.apache.tapestry.contrib.table.model.ITableSessionStateManager;
+import org.apache.tapestry.contrib.table.model.simple.SimpleListTableDataModel;
+import org.apache.tapestry.contrib.table.model.simple.SimpleTableColumnModel;
+import org.apache.tapestry.contrib.table.model.simple.SimpleTableModel;
+import org.apache.tapestry.contrib.table.model.simple.SimpleTableSessionStateManager;
+import org.apache.tapestry.contrib.tree.model.ITreeDataModel;
+import org.apache.tapestry.contrib.tree.model.ITreeModel;
+import org.apache.tapestry.contrib.tree.model.ITreeModelSource;
+import org.apache.tapestry.contrib.tree.model.ITreeRowSource;
+import org.apache.tapestry.contrib.tree.model.TreeRowObject;
+import org.apache.tapestry.event.PageDetachListener;
+import org.apache.tapestry.event.PageEvent;
+
+/**
+ * @version $Id$
+ */
+public class TreeTableDataView extends BaseComponent implements ITreeRowSource, PageDetachListener{
+    private int m_nTreeDeep = -1;
+	private TreeRowObject m_objTreeRowObject = null;
+	private ArrayList m_arrAllExpandedNodes = null;
+
+    public TreeTableDataView(){
+        super();
+        initialize();
+    }
+
+    public void initialize(){
+        m_nTreeDeep = -1;
+//		m_objTableModel = null;
+		m_objTreeRowObject = null;
+		m_arrAllExpandedNodes = null;
+    }
+
+
+	/**
+	 * @see org.apache.tapestry.AbstractComponent#finishLoad()
+	 */
+	protected void finishLoad() {
+		super.finishLoad();
+		getPage().addPageDetachListener(this);
+	}
+
+	/**
+	 * @see org.apache.tapestry.event.PageDetachListener#pageDetached(org.apache.tapestry.event.PageEvent)
+	 */
+	public void pageDetached(PageEvent arg0) {
+		initialize();
+	}
+
+
+    public ITreeModelSource getTreeModelSource() {
+		ITreeModelSource objSource = (ITreeModelSource) getPage().getRequestCycle().getAttribute(ITreeModelSource.TREE_MODEL_SOURCE_ATTRIBUTE);
+    	if(objSource == null){
+			objSource = (ITreeModelSource) getBinding("treeView").getObject();
+    	}
+    	return objSource;
+    }
+
+    public ArrayList generateNodeList() {
+        if(m_arrAllExpandedNodes == null){
+	        // render data
+			ITreeModelSource objTreeModelSource = getTreeModelSource();
+	        ITreeModel objTreeModel = objTreeModelSource.getTreeModel();
+	        ITreeDataModel objTreeDataModel = objTreeModel.getTreeDataModel();
+	        Object objValue = objTreeDataModel.getRoot();
+	        Object objValueUID = objTreeDataModel.getUniqueKey(objValue, null);
+	
+	        // Object objSelectedNode = objTreeModel.getTreeStateModel().getSelectedNode();
+	        //if(objSelectedNode == null)
+	        //  objTreeModel.getTreeStateModel().expand(objValueUID);
+			ArrayList arrAllExpandedNodes = new ArrayList();
+			walkTree(arrAllExpandedNodes, objValue, objValueUID, 0, objTreeModel);
+			m_arrAllExpandedNodes = arrAllExpandedNodes;
+		}
+		
+		
+		return m_arrAllExpandedNodes;
+    }
+
+    public void walkTree(ArrayList arrAllExpandedNodes, Object objParent, Object objParentUID, int nDepth,
+                         ITreeModel objTreeModel) {
+        m_nTreeDeep = nDepth;
+
+		TreeRowObject objTreeRowObject = new TreeRowObject(objParent, objParentUID, nDepth);
+		arrAllExpandedNodes.add(objTreeRowObject);
+
+        boolean bContain = objTreeModel.getTreeStateModel().isUniqueKeyExpanded(objParentUID);
+        if (bContain) {
+			Iterator colChildren = objTreeModel.getTreeDataModel().getChildren(objParent);
+            for (Iterator iter = colChildren; iter.hasNext();) {
+                Object objChild = iter.next();
+                Object objChildUID = objTreeModel.getTreeDataModel().getUniqueKey(objChild, objParentUID);
+                walkTree(arrAllExpandedNodes, objChild, objChildUID, nDepth+1, objTreeModel);
+            }
+        }
+    }
+
+    /**
+     * Returns the treeDeep.
+     * @return int
+     */
+    public int getTreeDeep() {
+        return m_nTreeDeep;
+    }
+
+/*	public ITableModel getTableModel() {
+		if(m_objTableModel == null){
+			m_objTableModel = createTableModel();
+		}
+		return m_objTableModel;
+	}
+*/
+	public ITableModel getTableModel() {
+		return createTableModel();
+	}
+
+	private ITableModel createTableModel(){
+		ArrayList arrAllNodes = generateNodeList();
+		Object[] arrAllExpandedNodes = new Object[arrAllNodes.size()];
+		arrAllNodes.toArray(arrAllExpandedNodes);
+
+		
+		SimpleTableModel objTableModel = new SimpleTableModel(arrAllExpandedNodes, getTableColunms());
+		objTableModel.getPagingState().setPageSize(getEntriesPerTablePage());		
+		
+		return objTableModel;
+	}
+
+	public ITableColumn[] getTableColunms(){
+		ArrayList arrColumnsList = new ArrayList();
+		arrColumnsList.add(new TreeTableColumn ("Name", false, null)); 
+
+		ArrayList arrTableColunms = getTableColunmsFromBinding();
+		if(arrTableColunms != null)
+			arrColumnsList.addAll(arrTableColunms);
+		
+		ITableColumn[] arrColumns = new ITableColumn[arrColumnsList.size()];
+		arrColumnsList.toArray(arrColumns);
+
+		return arrColumns;
+	}
+
+	public ArrayList getTableColunmsFromBinding(){
+		IBinding objBinding = getBinding("tableColunms");
+		if(objBinding != null)
+			return (ArrayList)objBinding.getObject();
+		return null;
+	}
+	
+	public int getEntriesPerTablePage(){
+		IBinding objBinding = getBinding("entriesPerTablePage");
+		if(objBinding != null)
+			return objBinding.getInt();
+		return 50;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeRowSource#getTreeRow()
+	 */
+	public TreeRowObject getTreeRow() {
+		return getTreeRowObject();
+	}
+
+	public ITableSessionStateManager getTableSessionStateManager(){
+		SimpleListTableDataModel objDataModel = new SimpleListTableDataModel(generateNodeList());
+		SimpleTableColumnModel objColumnModel = new SimpleTableColumnModel(getTableColunms());
+		SimpleTableSessionStateManager objStateManager = new SimpleTableSessionStateManager(objDataModel, objColumnModel);
+		return objStateManager;
+		//return NullTableSessionStateManager.NULL_STATE_MANAGER;
+	}
+
+	/**
+	 * @see org.apache.tapestry.BaseComponent#renderComponent(org.apache.tapestry.IMarkupWriter, org.apache.tapestry.IRequestCycle)
+	 */
+	protected void renderComponent(IMarkupWriter arg0, IRequestCycle cycle) {
+		Object objExistedTreeModelSource = cycle.getAttribute(ITreeRowSource.TREE_ROW_SOURCE_ATTRIBUTE);
+		cycle.setAttribute(ITreeRowSource.TREE_ROW_SOURCE_ATTRIBUTE, this);
+
+		super.renderComponent(arg0, cycle);
+
+		cycle.setAttribute(ITreeRowSource.TREE_ROW_SOURCE_ATTRIBUTE, objExistedTreeModelSource);
+	}
+
+	/**
+	 * @see org.apache.tapestry.AbstractComponent#renderBody(org.apache.tapestry.IMarkupWriter, org.apache.tapestry.IRequestCycle)
+	 */
+	public void renderBody(IMarkupWriter arg0, IRequestCycle cycle) {
+		Object objExistedTreeModelSource = cycle.getAttribute(ITreeRowSource.TREE_ROW_SOURCE_ATTRIBUTE);
+		cycle.setAttribute(ITreeRowSource.TREE_ROW_SOURCE_ATTRIBUTE, this);
+
+		super.renderBody(arg0, cycle);
+
+		cycle.setAttribute(ITreeRowSource.TREE_ROW_SOURCE_ATTRIBUTE, objExistedTreeModelSource);
+	}
+
+
+	public TreeRowObject getTreeRowObject() {
+		return m_objTreeRowObject;
+	}
+
+	public void setTreeRowObject(TreeRowObject object) {
+		m_objTreeRowObject = object;
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableDataView.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableDataView.jwc
new file mode 100644
index 0000000..41d2e20
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableDataView.jwc
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!--  $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.contrib.tree.components.table.TreeTableDataView"
+    allow-body="yes" allow-informal-parameters="yes">
+
+    <parameter name="treeView"
+               type="org.apache.tapestry.contrib.tree.components.TreeView"
+               required="no" direction="custom"/>
+
+    <parameter name="nodeViewComponentAddress"
+               type="org.apache.tapestry.util.ComponentAddress"
+               required="no" direction="custom"/>
+
+	<parameter name="entriesPerTablePage" 
+		type="int" 
+		required="no"
+		direction="custom"/>
+
+    <parameter name="tableColunms"
+               type="java.util.ArrayList"
+               required="no" direction="custom"/>
+
+    <bean name="tableClass" class="org.apache.tapestry.bean.EvenOdd" lifecycle="request"/>
+
+	<component id="table" type="Table">
+		<binding name="tableModel" expression="tableModel"/>
+		<binding name="tableSessionStateManager" expression="tableSessionStateManager"/>
+		<binding name="row" expression="treeRowObject"/>
+        <binding name="rowsClass" expression='beans.tableClass.next'/>
+	</component>
+
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableNodeViewDelegator.html b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableNodeViewDelegator.html
new file mode 100644
index 0000000..f868b5a
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableNodeViewDelegator.html
@@ -0,0 +1,3 @@
+<span jwcid="$content$">
+	<span jwcid="treeNodeView"/>
+</span>		
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableNodeViewDelegator.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableNodeViewDelegator.java
new file mode 100644
index 0000000..e38e50c
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableNodeViewDelegator.java
@@ -0,0 +1,46 @@
+//  Copyright 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.tapestry.contrib.tree.components.table;
+
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+import org.apache.tapestry.contrib.table.model.ITableModelSource;
+import org.apache.tapestry.contrib.table.model.ITableRendererListener;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public class TreeTableNodeViewDelegator extends BaseComponent implements ITableRendererListener{
+
+	/**
+	 * 
+	 */
+	public TreeTableNodeViewDelegator() {
+		super();
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableRendererListener#initializeRenderer(org.apache.tapestry.IRequestCycle, org.apache.tapestry.contrib.table.model.ITableModelSource, org.apache.tapestry.contrib.table.model.ITableColumn, java.lang.Object)
+	 */
+	public void initializeRenderer(
+		IRequestCycle arg0,
+		ITableModelSource arg1,
+		ITableColumn arg2,
+		Object arg3) {
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableNodeViewDelegator.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableNodeViewDelegator.jwc
new file mode 100644
index 0000000..eb55db7
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableNodeViewDelegator.jwc
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!--  $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.contrib.tree.components.table.TreeTableNodeViewDelegator"
+    allow-body="yes" allow-informal-parameters="yes">
+
+     <component id="treeNodeView" type="TreeNodeView">
+    </component>
+
+</component-specification>
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableNodeViewPage.html b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableNodeViewPage.html
new file mode 100644
index 0000000..e554dea
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableNodeViewPage.html
@@ -0,0 +1,5 @@
+<!-- generated by Spindle, http://spindle.sourceforge.net -->
+
+<span jwcid="$content$">
+	<span jwcid="treeTableNodeViewDelegator"/>
+</span>
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableNodeViewPage.page b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableNodeViewPage.page
new file mode 100644
index 0000000..855e862
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableNodeViewPage.page
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<!--
+    Copyright 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.
+-->
+<!-- Forms.page,v 1.1 2002/08/23 22:18:31 hship Exp -->
+<!DOCTYPE page-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<page-specification>
+
+    <component id="treeTableNodeViewDelegator" type="TreeTableNodeViewDelegator">
+    </component>
+
+</page-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableValueRenderSource.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableValueRenderSource.java
new file mode 100644
index 0000000..2057138
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/components/table/TreeTableValueRenderSource.java
@@ -0,0 +1,76 @@
+//  Copyright 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.tapestry.contrib.tree.components.table;
+
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.table.model.ITableColumn;
+import org.apache.tapestry.contrib.table.model.ITableModelSource;
+import org.apache.tapestry.contrib.table.model.ITableRendererSource;
+import org.apache.tapestry.contrib.table.model.common.ComponentTableRendererSource;
+import org.apache.tapestry.util.ComponentAddress;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public class TreeTableValueRenderSource implements ITableRendererSource
+{
+
+	private ComponentTableRendererSource m_objComponentRenderer;
+	private ComponentAddress m_objComponentAddress = null;
+
+	public TreeTableValueRenderSource()
+	{
+		m_objComponentRenderer = null;
+	}
+
+	public TreeTableValueRenderSource(ComponentAddress objComponentAddress)
+	{
+		m_objComponentAddress = objComponentAddress;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.table.model.ITableRendererSource#getRenderer(IRequestCycle, ITableModelSource, ITableColumn, Object)
+	 */
+	public IRender getRenderer(
+		IRequestCycle objCycle,
+		ITableModelSource objSource,
+		ITableColumn objColumn,
+		Object objRow)
+	{
+			synchronized (this)
+			{
+				if (m_objComponentRenderer == null)
+				{
+					
+					ComponentAddress objAddress = m_objComponentAddress;
+					if(m_objComponentAddress == null)
+						objAddress = new ComponentAddress(
+							"contrib:TreeTableNodeViewPage",
+							"treeTableNodeViewDelegator");
+					m_objComponentRenderer =
+						new ComponentTableRendererSource(objAddress);
+				}
+			}
+
+		return m_objComponentRenderer.getRenderer(
+			objCycle,
+			objSource,
+			objColumn,
+			objRow);
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/IMutableTreeNode.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/IMutableTreeNode.java
new file mode 100644
index 0000000..d27eef6
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/IMutableTreeNode.java
@@ -0,0 +1,54 @@
+//  Copyright 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.tapestry.contrib.tree.model;
+
+import java.util.Collection;
+
+/**
+ * Defines the requirements for a tree node object that can change --
+ * by adding or removing child nodes, or by changing the contents
+ * of a user object stored in the node.
+ *
+ * @see javax.swing.tree.DefaultMutableTreeNode
+ * @see javax.swing.JTree
+ *
+ * @author ceco
+ * @version $Id$
+ */
+
+public interface IMutableTreeNode extends ITreeNode
+{
+    /**
+     * Adds collection of<code>children</code> to the receiver.
+     * <code>Child</code> will be messaged with <code>setParent</code>.
+     */
+    void insert(Collection colChildren);
+
+    /**
+     * Removes <code>node</code> from the receiver. <code>setParent</code>
+     * will be messaged on <code>node</code>.
+     */
+    void remove(IMutableTreeNode node);
+
+    /**
+     * Removes the receiver from its parent.
+     */
+    void removeFromParent();
+
+    /**
+     * Sets the parent of the receiver to <code>newParent</code>.
+     */
+    void setParent(IMutableTreeNode newParent);
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ISessionStoreManager.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ISessionStoreManager.java
new file mode 100644
index 0000000..fb5644d
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ISessionStoreManager.java
@@ -0,0 +1,27 @@
+//  Copyright 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.tapestry.contrib.tree.model;
+
+import org.apache.tapestry.IPage;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+
+public interface ISessionStoreManager {
+	Object getSessionState(IPage objPage, String strSessionStateID);
+	Object setSessionState(IPage objPage, String strSessionStateID, Object objSessionState);
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeDataModel.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeDataModel.java
new file mode 100644
index 0000000..9522eb0
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeDataModel.java
@@ -0,0 +1,73 @@
+//  Copyright 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.tapestry.contrib.tree.model;
+
+import java.util.Iterator;
+
+/**
+ * The interface that defines a suitable data model for a <code>TreeView component</code>. 
+ * 
+ * @author ceco
+ * @version $Id$
+ */
+public interface ITreeDataModel
+{
+	/**
+	 * Returns the root node of the tree
+	 */
+	Object getRoot();
+
+	/**
+	 * Returns the number of children of parent node.
+	 * @param objParent is the parent object whose nr of children are sought
+	 */
+	int getChildCount(Object objParent);
+
+	/**
+	 * Get an iterator to the Collection of children belonging to the parent node object
+	 * @param objParent is the parent object whose children are requested
+	 */
+	Iterator getChildren(Object objParent);
+
+	/**
+	 * Get the actual node object based on some identifier (for example an UUID)
+	 * @param objUniqueKey is the unique identifier of the node object being retrieved
+	 * @return the instance of the node object identified by the key
+	 */
+	Object getObject(Object objUniqueKey);
+
+	/** 
+	 * Get the unique identifier (UUID) of the node object with a certain parent node
+	 * @param objTarget is the Object whose identifier is required
+	 * @param objParentUniqueKey is the unique id of the parent of objTarget
+	 * @return the unique identifier of objTarget
+	 */
+	Object getUniqueKey(Object objTarget, Object objParentUniqueKey);
+
+	/**
+	 * Get the unique identifier of the parent of an object
+	 * @param objChildUniqueKey is the identifier of the Object for which the parent identifier is sought
+	 * @return the identifier (possibly UUID) of the parent of objChildUniqueKey
+	 */
+	Object getParentUniqueKey(Object objChildUniqueKey);
+
+	/**
+	 * Check to see (on the basis of some node object identifier) whether the parent node is indeed the parent of the object
+	 * @param objChildUniqueKey is the identifier of the object whose parent is being checked
+	 * @param objParentUniqueKey is the identifier of the parent which is to be checked against
+	 */
+	boolean isAncestorOf(Object objChildUniqueKey, Object objParentUniqueKey);
+	
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeModel.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeModel.java
new file mode 100644
index 0000000..04902ce
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeModel.java
@@ -0,0 +1,24 @@
+//  Copyright 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.tapestry.contrib.tree.model;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public interface ITreeModel {
+	ITreeDataModel getTreeDataModel();
+	ITreeStateModel getTreeStateModel();
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeModelSource.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeModelSource.java
new file mode 100644
index 0000000..75c751d
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeModelSource.java
@@ -0,0 +1,30 @@
+//  Copyright 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.tapestry.contrib.tree.model;
+
+import org.apache.tapestry.IComponent;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public interface ITreeModelSource extends IComponent
+{
+    final static String TREE_MODEL_SOURCE_ATTRIBUTE = "org.apache.tapestry.contrib.tree.model.ITreeModelSource";
+
+	ITreeModel getTreeModel();
+	ITreeStateListener getTreeStateListener();
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeNode.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeNode.java
new file mode 100644
index 0000000..7e08fda
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeNode.java
@@ -0,0 +1,59 @@
+//  Copyright 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.tapestry.contrib.tree.model;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+
+public interface ITreeNode extends Serializable
+{
+	
+    /**
+     * Returns the <code>Collection</code> of children. 
+     */
+    Collection getChildren();
+
+    /**
+     * Returns the number of children <code>ITreeNode</code>s the receiver
+     * contains.
+     */
+    int getChildCount();
+
+    /**
+     * Returns the parent <code>ITreeNode</code> of the receiver.
+     */
+    ITreeNode getParent();
+
+    /**
+     * Returns the true if current node contains received children, otherwise return false;
+     */
+    boolean containsChild(ITreeNode node);
+
+    /**
+     * Returns true if the receiver allows children.
+     */
+    boolean getAllowsChildren();
+
+    /**
+     * Returns true if the receiver is a leaf.
+     */
+    boolean isLeaf();
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeNodeManager.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeNodeManager.java
new file mode 100644
index 0000000..dbd03d8
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeNodeManager.java
@@ -0,0 +1,26 @@
+//  Copyright 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.tapestry.contrib.tree.model;
+
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.contrib.tree.components.ITreeComponent;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public interface ITreeNodeManager {
+	IRender getRenderer(Object objUniqueKey, ITreeComponent objTreeComponent);
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeRowSource.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeRowSource.java
new file mode 100644
index 0000000..5f2ff40
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeRowSource.java
@@ -0,0 +1,36 @@
+//  Copyright 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.tapestry.contrib.tree.model;
+
+/**
+ * A Tapestry component that provides the current row value.
+ * This interface is used for obtaining the row source by components 
+ * wrapped by the row source
+ * 
+ * @version $Id$
+ * @author tsvetelin
+ */
+public interface ITreeRowSource
+{
+    final static String TREE_ROW_SOURCE_ATTRIBUTE = "org.apache.tapestry.contrib.tree.model.ITreeRowSource";
+
+	/**
+	 * Method getTreeRow
+	 * @return Object the current tree row object.
+	 */
+	TreeRowObject getTreeRow();
+	//Object getTreeRowNodeUID();
+	//int getTreeNodeDeep();
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeSessionStateManager.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeSessionStateManager.java
new file mode 100644
index 0000000..00dbfa2
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeSessionStateManager.java
@@ -0,0 +1,26 @@
+//  Copyright 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.tapestry.contrib.tree.model;
+
+
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public interface ITreeSessionStateManager {
+	Object getSessionState(ITreeModel objModel);
+	ITreeModel getModel(Object objSessionState);	
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeStateListener.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeStateListener.java
new file mode 100644
index 0000000..cddf084
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeStateListener.java
@@ -0,0 +1,23 @@
+//  Copyright 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.tapestry.contrib.tree.model;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public interface ITreeStateListener {
+	void treeStateChanged(TreeStateEvent objEvent);
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeStateModel.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeStateModel.java
new file mode 100644
index 0000000..9286946
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/ITreeStateModel.java
@@ -0,0 +1,39 @@
+//  Copyright 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.tapestry.contrib.tree.model;
+
+import java.util.Set;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+
+public interface ITreeStateModel {
+	Set getExpandSelection();
+	/*
+	 * Return the selected node unique key
+	 */
+	Object getSelectedNode();
+
+	void expand(Object objUniqueKey);
+	void expandPath(Object objUniqueKey);
+	void collapse(Object objUniqueKey);
+	void collapsePath(Object objUniqueKey);
+
+	boolean isUniqueKeyExpanded(Object objUniqueKey);
+	
+	void resetState();
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/TreeRowObject.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/TreeRowObject.java
new file mode 100644
index 0000000..697c3f8
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/TreeRowObject.java
@@ -0,0 +1,45 @@
+//  Copyright 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.tapestry.contrib.tree.model;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public class TreeRowObject {
+	private Object m_objTreeNode = null;
+	private Object m_objTreeNodeUID = null;
+	private int m_nTreeRowDepth;
+
+	public TreeRowObject(Object objTreeNode, Object objTreeNodeUID, int nTreeRowDepth) {
+		super();
+		m_objTreeNode = objTreeNode;
+		m_objTreeNodeUID = objTreeNodeUID;
+		m_nTreeRowDepth = nTreeRowDepth;
+	}
+
+	public Object getTreeNode() {
+		return m_objTreeNode;
+	}
+
+	public Object getTreeNodeUID() {
+		return m_objTreeNodeUID;
+	}
+
+	public int getTreeRowDepth() {
+		return m_nTreeRowDepth;
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/TreeStateEvent.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/TreeStateEvent.java
new file mode 100644
index 0000000..86af620
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/model/TreeStateEvent.java
@@ -0,0 +1,59 @@
+//  Copyright 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.tapestry.contrib.tree.model;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public class TreeStateEvent {
+	public static final int SELECTED_NODE_CHANGED 	= 1;
+	public static final int NODE_EXPANDED 			= 2;
+	public static final int NODE_COLLAPSED 			= 4;
+	
+	private int m_nEventType;
+	private transient ITreeStateModel m_objTreeStateModel = null;
+	private transient Object m_objNodeUID = null;
+
+	/**
+	 * Constructor for TreeStateEvent.
+	 */
+	public TreeStateEvent(int nEventType, Object objNodeUID, ITreeStateModel objTreeStateModel) {
+		super();
+		m_nEventType = nEventType;
+		m_objNodeUID = objNodeUID;
+		m_objTreeStateModel = objTreeStateModel;
+	}
+
+	/**
+	 * Returns the EventType.
+	 * @return int
+	 */
+	public int getEventType() {
+		return m_nEventType;
+	}
+
+    public boolean isEvent(int nEventType){
+		return (getEventType() & nEventType) > 0;
+	}
+
+	public Object getNodeUID() {
+		return m_objNodeUID;
+	}
+
+	public ITreeStateModel getTreeStateModel() {
+		return m_objTreeStateModel;
+	}
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/FullTreeSessionStateManager.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/FullTreeSessionStateManager.java
new file mode 100644
index 0000000..14771df
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/FullTreeSessionStateManager.java
@@ -0,0 +1,47 @@
+//  Copyright 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.tapestry.contrib.tree.simple;
+
+import org.apache.tapestry.contrib.tree.model.ITreeModel;
+import org.apache.tapestry.contrib.tree.model.ITreeSessionStateManager;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public class FullTreeSessionStateManager implements ITreeSessionStateManager {
+
+	/**
+	 * Constructor for FullTreeSessionStateManager.
+	 */
+	public FullTreeSessionStateManager() {
+		super();
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeSessionStateManager#getSessionState(ITreeModel)
+	 */
+	public Object getSessionState(ITreeModel objModel) {
+		return objModel;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeSessionStateManager#getModel(Object)
+	 */
+	public ITreeModel getModel(Object objSessionState) {
+		return (ITreeModel)objSessionState;
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/NullSessionStateManager.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/NullSessionStateManager.java
new file mode 100644
index 0000000..2294228
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/NullSessionStateManager.java
@@ -0,0 +1,47 @@
+//  Copyright 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.tapestry.contrib.tree.simple;
+
+import org.apache.tapestry.contrib.tree.model.ITreeModel;
+import org.apache.tapestry.contrib.tree.model.ITreeSessionStateManager;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public class NullSessionStateManager implements ITreeSessionStateManager {
+
+	/**
+	 * Constructor for NullSessionStateManager.
+	 */
+	public NullSessionStateManager() {
+		super();
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeSessionStateManager#getSessionState(ITreeModel)
+	 */
+	public Object getSessionState(ITreeModel objModel) {
+		return null;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeSessionStateManager#getModel(Object)
+	 */
+	public ITreeModel getModel(Object objSessionState) {
+		return null;
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/SimpleNodeRenderFactory.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/SimpleNodeRenderFactory.java
new file mode 100644
index 0000000..4696bda
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/SimpleNodeRenderFactory.java
@@ -0,0 +1,55 @@
+//  Copyright 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.tapestry.contrib.tree.simple;
+
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.contrib.tree.components.INodeRenderFactory;
+import org.apache.tapestry.contrib.tree.model.ITreeModelSource;
+import org.apache.tapestry.valid.RenderString;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public class SimpleNodeRenderFactory implements INodeRenderFactory {
+
+	/**
+	 * Constructor for SimpleNodeRenderFactory.
+	 */
+	public SimpleNodeRenderFactory() {
+		super();
+	}
+
+	/**
+	 * @see INodeRenderFactory#getRender
+	 */
+	public IRender getRenderByID(
+		Object objUniqueKey,
+		ITreeModelSource objTreeModelSource,
+		IRequestCycle cycle)
+	{
+		Object objValue = objTreeModelSource.getTreeModel().getTreeDataModel().getObject(objUniqueKey);
+		return getRender(objValue, objTreeModelSource, cycle);
+	}
+
+	/**
+	 * @see INodeRenderFactory#getRender
+	 */
+	public IRender getRender(Object objValue, ITreeModelSource objTreeModelSource, IRequestCycle objCycle) {
+		return new RenderString(objValue.toString());
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/SimpleSessionStateManager.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/SimpleSessionStateManager.java
new file mode 100644
index 0000000..c7f8ee5
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/SimpleSessionStateManager.java
@@ -0,0 +1,47 @@
+//  Copyright 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.tapestry.contrib.tree.simple;
+
+import org.apache.tapestry.contrib.tree.model.ITreeModel;
+import org.apache.tapestry.contrib.tree.model.ITreeSessionStateManager;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public class SimpleSessionStateManager implements ITreeSessionStateManager {
+
+	/**
+	 * Constructor for SimpleSessionStateManager.
+	 */
+	public SimpleSessionStateManager() {
+		super();
+	}
+
+	/**
+	 * @see ITreeSessionStateManager#getSessionState(ITreeModel)
+	 */
+	public Object getSessionState(ITreeModel objModel) {
+		return objModel;
+	}
+
+	/**
+	 * @see ITreeSessionStateManager#getModel(Object)
+	 */
+	public ITreeModel getModel(Object objSessionState) {
+		return (ITreeModel)objSessionState;
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/SimpleTreeDataModel.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/SimpleTreeDataModel.java
new file mode 100644
index 0000000..71678c7
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/SimpleTreeDataModel.java
@@ -0,0 +1,110 @@
+//  Copyright 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.tapestry.contrib.tree.simple;
+
+import java.io.Serializable;
+import java.util.Iterator;
+
+import javax.swing.tree.TreePath;
+
+import org.apache.tapestry.contrib.tree.model.ITreeDataModel;
+import org.apache.tapestry.contrib.tree.model.ITreeNode;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public class SimpleTreeDataModel implements ITreeDataModel, Serializable {
+
+	protected ITreeNode m_objRootNode;
+	/**
+	 * Constructor for SimpleTreeDataModel.
+	 */
+	public SimpleTreeDataModel(ITreeNode objRootNode) {
+		super();
+		m_objRootNode = objRootNode;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeDataModel#getRoot()
+	 */
+	public Object getRoot() {
+		return m_objRootNode;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeDataModel#getChildCount(Object)
+	 */
+	public int getChildCount(Object objParent) {
+		ITreeNode objParentNode = (ITreeNode)objParent;
+		
+		return objParentNode.getChildCount();
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeDataModel#getChildren(Object)
+	 */
+	public Iterator getChildren(Object objParent) {
+		ITreeNode objParentNode = (ITreeNode)objParent;
+		return objParentNode.getChildren().iterator();
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeDataModel#getObject(Object)
+	 */
+	public Object getObject(Object objUniqueKey) {
+		if(objUniqueKey != null) {
+			TreePath objPath = (TreePath)objUniqueKey;
+			return objPath.getLastPathComponent();
+		}
+		return null;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeDataModel#getUniqueKey(Object, Object)
+	 */
+	public Object getUniqueKey(Object objTarget, Object objParentUniqueKey) {
+		TreePath objPath = (TreePath)objParentUniqueKey;
+		Object objTargetUID = null;
+		if(objPath != null){
+			objTargetUID = objPath.pathByAddingChild(objTarget);
+		}else{
+			objTargetUID = new TreePath(objTarget);
+		}
+		return objTargetUID;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeDataModel#isAncestorOf(Object, Object)
+	 */
+	public boolean isAncestorOf(Object objTargetUniqueKey, Object objParentUniqueKey) {
+		TreePath objParentPath = (TreePath)objParentUniqueKey;
+		TreePath objTargetPath = (TreePath)objTargetUniqueKey;
+		boolean bResult = objParentPath.isDescendant(objTargetPath);
+		return bResult;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeDataModel#getParentUniqueKey
+	 */
+	public Object getParentUniqueKey(Object objChildUniqueKey) {
+		TreePath objChildPath = (TreePath)objChildUniqueKey;
+		TreePath objParentPath = objChildPath.getParentPath();
+		if(objParentPath == null)
+			return null;
+		return objParentPath.getLastPathComponent();
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/SimpleTreeModel.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/SimpleTreeModel.java
new file mode 100644
index 0000000..35c51bc
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/SimpleTreeModel.java
@@ -0,0 +1,58 @@
+//  Copyright 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.tapestry.contrib.tree.simple;
+
+import java.io.Serializable;
+
+import org.apache.tapestry.contrib.tree.model.ITreeDataModel;
+import org.apache.tapestry.contrib.tree.model.ITreeModel;
+import org.apache.tapestry.contrib.tree.model.ITreeStateModel;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public class SimpleTreeModel implements ITreeModel, Serializable{
+
+	private ITreeDataModel m_objDataModel;
+	private ITreeStateModel m_objTreeStateModel;
+	
+	/**
+	 * Constructor for SimpleTreeModel.
+	 */
+	public SimpleTreeModel(ITreeDataModel objDataModel) {
+		this(objDataModel, new SimpleTreeStateModel());
+	}
+
+	public SimpleTreeModel(ITreeDataModel objDataModel, ITreeStateModel objTreeStateModel) {
+		super();
+		m_objDataModel = objDataModel;
+		m_objTreeStateModel = objTreeStateModel;
+	}
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeModel#getTreeDataModel()
+	 */
+	public ITreeDataModel getTreeDataModel() {
+		return m_objDataModel;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeModel#getTreeStateModel()
+	 */
+	public ITreeStateModel getTreeStateModel() {
+		return m_objTreeStateModel;
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/SimpleTreeStateModel.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/SimpleTreeStateModel.java
new file mode 100644
index 0000000..88d0e8c
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/SimpleTreeStateModel.java
@@ -0,0 +1,106 @@
+//  Copyright 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.tapestry.contrib.tree.simple;
+
+import java.io.Serializable;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.tapestry.contrib.tree.model.ITreeStateModel;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public class SimpleTreeStateModel implements ITreeStateModel, Serializable{
+
+	private Set m_setExpanded;
+	private Object m_objSelectedNodeUID = null;
+	
+	/**
+	 * Constructor for SimpleTreeStateModel.
+	 */
+	public SimpleTreeStateModel() {
+		super();
+		initialize();
+	}
+	private void initialize(){
+		m_setExpanded = new HashSet();
+		m_objSelectedNodeUID = null;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeStateModel#getExpandSelection()
+	 */
+	public Set getExpandSelection() {
+		return m_setExpanded;
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeStateModel#expand(Object)
+	 */
+	public void expand(Object objUniqueKey) {
+		m_setExpanded.add(objUniqueKey);
+		setSelectedNode(objUniqueKey);
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeStateModel#expandPath(Object)
+	 */
+	public void expandPath(Object objUniqueKey) {
+		m_setExpanded.add(objUniqueKey);
+		setSelectedNode(objUniqueKey);
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeStateModel#collapse(Object)
+	 */
+	public void collapse(Object objUniqueKey) {
+		m_setExpanded.remove(objUniqueKey);
+		setSelectedNode(objUniqueKey);
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeStateModel#collapsePath(Object)
+	 */
+	public void collapsePath(Object objUniqueKey) {
+		m_setExpanded.remove(objUniqueKey);
+		setSelectedNode(objUniqueKey);
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeStateModel#isUniqueKeyExpanded(Object)
+	 */
+	public boolean isUniqueKeyExpanded(Object objUniqueKey) {
+		return m_setExpanded.contains(objUniqueKey);
+	}
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeStateModel#getSelectedNode()
+	 */
+	public Object getSelectedNode() {
+		return m_objSelectedNodeUID;
+	}
+	private void setSelectedNode(Object objUniqueKey){
+		if(m_objSelectedNodeUID == null || !m_objSelectedNodeUID.equals(objUniqueKey))
+			m_objSelectedNodeUID = objUniqueKey;
+	}
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeStateModel#resetState()
+	 */
+	public void resetState() {
+		initialize();
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/TreeNode.java b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/TreeNode.java
new file mode 100644
index 0000000..ba15fe6
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/tree/simple/TreeNode.java
@@ -0,0 +1,108 @@
+//  Copyright 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.tapestry.contrib.tree.simple;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import org.apache.tapestry.contrib.tree.model.IMutableTreeNode;
+import org.apache.tapestry.contrib.tree.model.ITreeNode;
+
+/**
+ * @author ceco
+ * @version $Id$
+ */
+public class TreeNode implements IMutableTreeNode {
+
+	protected Set m_setChildren;
+	protected IMutableTreeNode m_objParentNode;
+	
+	/**
+	 * Constructor for TreeNode.
+	 */
+	public TreeNode() {
+		this(null);
+	}
+	public TreeNode(IMutableTreeNode parentNode) {
+		super();
+		m_objParentNode = parentNode;
+		m_setChildren = new HashSet();
+	}
+
+
+	public int getChildCount() {
+		return m_setChildren.size();
+	}
+
+	public ITreeNode getParent() {
+		return m_objParentNode;
+	}
+
+	public boolean getAllowsChildren() {
+		return true;
+	}
+
+	public boolean isLeaf() {
+		return m_setChildren.size() == 0 ? true:false;
+	}
+
+	public Collection children() {
+		return m_setChildren;
+	}
+
+
+	public void insert(IMutableTreeNode child) {
+		child.setParent(this);
+		m_setChildren.add(child);
+	}
+
+	public void remove(IMutableTreeNode node) {
+		m_setChildren.remove(node);
+	}
+
+	public void removeFromParent() {
+		m_objParentNode.remove(this);
+		m_objParentNode = null;
+	}
+
+	public void setParent(IMutableTreeNode newParent) {
+		m_objParentNode = newParent;
+	}
+
+	public void insert(Collection colChildren){
+		for (Iterator iter = colChildren.iterator(); iter.hasNext();) {
+			IMutableTreeNode element = (IMutableTreeNode) iter.next();
+			element.setParent(this);
+			m_setChildren.add(element);
+		}
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeNode#containsChild(ITreeNode)
+	 */
+	public boolean containsChild(ITreeNode node) {
+		return m_setChildren.contains(node);
+	}
+
+	/**
+	 * @see org.apache.tapestry.contrib.tree.model.ITreeNode#getChildren()
+	 */
+	public Collection getChildren() {
+		return m_setChildren;
+	}
+
+}
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/valid/DateField.java b/tapestry-contrib/src/org/apache/tapestry/contrib/valid/DateField.java
new file mode 100644
index 0000000..fca6a21
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/valid/DateField.java
@@ -0,0 +1,240 @@
+//  Copyright 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.tapestry.contrib.valid;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.valid.DateValidator;
+import org.apache.tapestry.valid.IValidator;
+import org.apache.tapestry.valid.ValidField;
+
+/**
+ *
+ *  Backwards compatible version of the 1.0.7 DateField component.
+ *
+ * <table border=1>
+ * <tr>
+ *    <td>Parameter</td>
+ *    <td>Type</td>
+ *	  <td>Read / Write </td>
+ *    <td>Required</td>
+ *    <td>Default</td>
+ *    <td>Description</td>
+ * </tr>
+ *
+ *  <tr>
+ *      <td>date</td>
+ *      <td>java.util.Date</td>
+ *      <td>R / W</td>
+ *      <td>yes</td>
+ *      <td>&nbsp;</td>
+ *      <td>The date property to edit.</td>
+ *  </tr>
+ *
+ *  <tr>
+ *      <td>required</td>
+ *      <td>boolean</td>
+ *      <td>R</td>
+ *      <td>no</td>
+ *      <td>no</td>
+ *      <td>If true, then a value must be entered.</td>
+ *  </tr>
+ *
+ *  <tr>
+ *      <td>minimum</td>
+ *      <td>java.util.Date</td>
+ *      <td>R</td>
+ *      <td>no</td>
+ *      <td>&nbsp;</td>
+ *      <td>If provided, the date entered must be equal to or later than the
+ *  provided minimum date.</td>
+ *  </tr>
+ *
+ *  <tr>
+ *      <td>maximum</td>
+ *      <td>java.util.Date</td>
+ *      <td>R</td>
+ *      <td>no</td>
+ *		<td>&nbsp;</td>
+ *      <td>If provided, the date entered must be less than or equal to the
+ *  provided maximum date.</td>
+ * </tr>
+ *
+ *  <tr>
+ *      <td>displayName</td>
+ *      <td>String</td>
+ *      <td>R</td>
+ *      <td>yes</td>
+ *      <td>&nbsp;</td>
+ *      <td>A textual name for the field that is used when formulating error messages.
+ *      </td>
+ *  </tr>
+ *
+ *  <tr>
+ *		<td>format</td>
+ *		<td>{@link DateFormat}</td>
+ *		<td>R</td>
+ *		<td>no</td>
+ *		<td>Default format <code>MM/dd/yyyy</code></td>
+ *		<td>The format used to display and parse dates.</td>
+ *	</tr>
+ *
+ *  <tr>
+ *		<td>displayFormat</td>
+ *		<td>{@link String}</td>
+ *		<td>R</td>
+ *		<td>no</td>
+ *		<td><code>MM/DD/YYYY</code></td>
+ *		<td>The format string presented to the user if the date entered is in an 
+ *   incorrect format. e.g. the format object throws a ParseException.</td>
+ *	</tr>
+ *
+ *  </table>
+ *
+ *  <p>Informal parameters are allowed.  A body is not allowed.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.8
+ * 
+ *  @see ValidField
+ * 
+ **/
+
+public abstract class DateField extends ValidField
+{
+	public abstract Date getDate();
+	public abstract void setDate(Date date);
+	
+    private IBinding minimumBinding;
+    private IBinding maximumBinding;
+    private IBinding requiredBinding;
+    private IBinding formatBinding;
+    private IBinding displayFormatBinding;
+
+
+    /**
+     *  Overrides {@link ValidField#getValidator()} to construct a validator
+     *  on-the-fly.
+     * 
+     **/
+
+    public IValidator getValidator()
+    {
+        DateValidator validator = new DateValidator();
+
+        if (minimumBinding != null)
+        {
+            Date minimum = (Date) minimumBinding.getObject("minimum", Date.class);
+            validator.setMinimum(minimum);
+        }
+
+        if (maximumBinding != null)
+        {
+            Date maximum = (Date) maximumBinding.getObject("maximum", Date.class);
+            validator.setMaximum(maximum);
+        }
+
+        if (requiredBinding != null)
+        {
+            boolean required = requiredBinding.getBoolean();
+            validator.setRequired(required);
+        }
+
+        if (formatBinding != null)
+        {
+            DateFormat format =
+                (DateFormat) formatBinding.getObject("format", DateFormat.class);
+            validator.setFormat(format);
+        }
+
+        if (displayFormatBinding != null)
+        {
+            String displayFormat =
+                (String) displayFormatBinding.getObject("displayFormat", String.class);
+            validator.setDisplayFormat(displayFormat);
+        }
+
+        return validator;
+    }
+
+    public IBinding getRequiredBinding()
+    {
+        return requiredBinding;
+    }
+
+    public void setRequiredBinding(IBinding requiredBinding)
+    {
+        this.requiredBinding = requiredBinding;
+    }
+
+    public IBinding getFormatBinding()
+    {
+        return formatBinding;
+    }
+
+    public void setFormatBinding(IBinding formatBinding)
+    {
+        this.formatBinding = formatBinding;
+    }
+    
+    public IBinding getDisplayFormatBinding()
+    {
+    	return displayFormatBinding;
+    }
+    
+    public void setDisplayFormatBinding(IBinding displayFormatBinding)
+    {
+    	this.displayFormatBinding = displayFormatBinding;
+    }
+    
+    public IBinding getMinimumBinding() {
+        return minimumBinding;
+    }
+    
+    public void setMinimumBinding(IBinding value)
+    {
+        this.minimumBinding = value;
+    }
+    
+    public IBinding getMaximumBinding()
+    {
+        return maximumBinding;
+    }
+    
+    public void setMaximumBinding(IBinding value)
+    {
+        this.maximumBinding = value;
+    }
+
+    /**
+     * @see org.apache.tapestry.valid.ValidField#getValue()
+     */
+    public Object getValue()
+    {
+        return getDate();
+    }
+
+    /**
+     * @see org.apache.tapestry.valid.ValidField#setValue(java.lang.Object)
+     */
+    public void setValue(Object value)
+    {
+        setDate((Date) value);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/valid/DateField.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/valid/DateField.jwc
new file mode 100644
index 0000000..d3fe179
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/valid/DateField.jwc
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.contrib.valid.DateField">
+
+  <parameter name="disabled" type="boolean" direction="in"/>
+  <parameter name="hidden" type="boolean" direction="in"/>
+  <parameter name="displayWidth" type="int" direction="in"/>
+  <parameter name="maximumLength" type="int" direction="in"/>
+
+  <parameter name="date" type="java.util.Date" direction="auto" required="yes" />
+  <parameter name="displayName" type="java.lang.String" direction="auto" required="yes" />
+  <parameter name="maximum" type="java.util.Date" />
+  <parameter name="minimum" type="java.util.Date" />
+  <parameter name="required" />
+  <parameter name="format" type="java.text.DateFormat" />
+  <parameter name="displayFormat" type="java.lang.String" />
+    
+  <reserved-parameter name="name"/>
+  <reserved-parameter name="type"/>
+  <reserved-parameter name="value"/>
+  <reserved-parameter name="size"/>
+  <reserved-parameter name="maxlength"/>
+
+  <property-specification name="name" type="java.lang.String"/>
+  <property-specification name="form" type="org.apache.tapestry.IForm"/>
+
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/valid/NumericField.java b/tapestry-contrib/src/org/apache/tapestry/contrib/valid/NumericField.java
new file mode 100644
index 0000000..2903ed9
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/valid/NumericField.java
@@ -0,0 +1,198 @@
+//  Copyright 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.tapestry.contrib.valid;
+
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.valid.IValidator;
+import org.apache.tapestry.valid.NumberValidator;
+import org.apache.tapestry.valid.ValidField;
+
+/**
+ *
+ * Backwards compatible version of the 1.0.7 NumericField component.
+ *
+ * <table border=1>
+ * <tr>
+ *    <td>Parameter</td>
+ *    <td>Type</td>
+ *	  <td>Read / Write </td>
+ *    <td>Required</td>
+ *    <td>Default</td>
+ *    <td>Description</td>
+ * </tr>
+ *
+ *  <tr>
+ *    <td>value</td>
+ *    <td>{@link Number}</td>
+ *    <td>R / W</td>
+ *   	<td>yes</td>
+ *		<td>&nbsp;</td>
+ *		<td>The value to be updated.
+ *
+ *  <p>When the form is submitted, this parameter is only updated if the value
+ *  is valid.
+ *
+ *  <p>When rendering, a null value will render as the empty string.  A value
+ *  of zero will render normally.
+ *
+ *  <p>When the form is submitted, the type of the binding
+ *  is used to determine what kind of object to convert the string to.
+ *
+ * </td>
+ *	</tr>
+ *
+ *  <tr>
+ *      <td>minimum</td>
+ *      <td>{@link Number}</td>
+ *      <td>R</td>
+ *      <td>no</td>
+ *      <td>&nbsp;</td>
+ *      <td>The minimum value accepted for the field.</td>
+ *  </tr>
+ *
+ *  <tr>
+ *      <td>maximum</td>
+ *      <td>{@link Number}</td>
+ *      <td>R</td>
+ *      <td>no</td>
+ *      <td>&nbsp;</td>
+ *      <td>The maximum value accepted for the field.</td>
+ * </tr>
+ *
+ *  <tr>
+ *      <td>required</td>
+ *      <td>boolean</td>
+ *      <td>R</td>
+ *      <td>no</td>
+ *      <td>false</td>
+ *      <td>If true, then a non-null value must be provided. If the field is not
+ * required, and a null (all whitespace) value is supplied in the field, then the
+ * value parameter is <em>not</em> updated.</td>
+ *  </tr>
+ *
+ *  <tr>
+ *      <td>displayName</td>
+ *      <td>String</td>
+ *      <td>R</td>
+ *      <td>yes</td>
+ *      <td>&nbsp;</td>
+ *      <td>A textual name for the field that is used when formulating error messages.
+ *      </td>
+ *  </tr>
+ * 
+ *  <tr>
+ *      <td>type</td>
+ *      <td>String</td>
+ *      <td>R</td>
+ *      <td>yes</td>
+ *      <td>&nbsp;</td>
+ *      <td>The class name used to convert the value entered.  See {@link NumberValidator#setValueType(String)}
+ *      </td>
+ *  </tr>
+ *
+ *	</table>
+ *
+ *  <p>May not contain a body.  May have informal parameters.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.8
+ *  @see ValidField
+ *
+ **/
+
+public abstract class NumericField extends ValidField
+{
+    private IBinding minimumBinding;
+    private IBinding maximumBinding;
+	private IBinding requiredBinding;
+	private IBinding typeBinding;
+
+	public IBinding getMinimumBinding()
+    {
+        return minimumBinding;
+    }
+
+    public void setMinimumBinding(IBinding value)
+    {
+        minimumBinding = value;
+    }
+
+    public IBinding getMaximumBinding()
+    {
+        return maximumBinding;
+    }
+
+    public void setMaximumBinding(IBinding value)
+    {
+        maximumBinding = value;
+    }
+
+	public IBinding getRequiredBinding()
+	{
+		return requiredBinding;
+	}
+
+	public void setRequiredBinding(IBinding requiredBinding)
+	{
+		this.requiredBinding = requiredBinding;
+	}
+
+	public IBinding getTypeBinding()
+	{
+		return typeBinding;
+	}
+
+	public void setTypeBinding(IBinding typeNameBinding)
+	{
+		this.typeBinding = typeNameBinding;
+	}
+
+    /**
+     * Overrides {@link ValidField#getValidator()} to construct
+     * a validator on the fly.
+     **/
+
+    public IValidator getValidator()
+    {
+        NumberValidator validator = new NumberValidator();
+
+        if (minimumBinding != null)
+        {
+            Number minimum = (Number) minimumBinding.getObject("minimum", Number.class);
+            validator.setMinimum(minimum);
+        }
+
+        if (maximumBinding != null)
+        {
+            Number maximum = (Number) maximumBinding.getObject("maximum", Number.class);
+            validator.setMaximum(maximum);
+        }
+
+        if (requiredBinding != null)
+        {
+            boolean required = requiredBinding.getBoolean();
+            validator.setRequired(required);
+        }
+
+        if (typeBinding != null)
+        {
+        	validator.setValueType(typeBinding.getString());
+        }
+        
+        return validator;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/valid/NumericField.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/valid/NumericField.jwc
new file mode 100644
index 0000000..f37d3fc
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/valid/NumericField.jwc
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.contrib.valid.NumericField">
+
+  <parameter name="disabled" type="boolean" direction="in"/>
+  <parameter name="hidden" type="boolean" direction="in"/>
+  <parameter name="displayWidth" type="int" direction="in" />
+  <parameter name="maximumLength" type="int" direction="in" />
+
+  <parameter name="value" type="java.lang.Object" direction="auto" required="yes"/>
+  <parameter name="displayName" type="java.lang.String" direction="auto" required="yes"/>
+  <parameter name="maximum" type="java.lang.Number"/>
+  <parameter name="minimum" type="java.lang.Number"/>
+  <parameter name="required"/>
+  <parameter name="type" type="java.lang.String" required="yes" direction="in" />
+
+  <reserved-parameter name="name"/>
+  
+  <property-specification name="name" type="java.lang.String"/>
+  <property-specification name="form" type="org.apache.tapestry.IForm"/>
+
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/valid/ValidatingTextField.java b/tapestry-contrib/src/org/apache/tapestry/contrib/valid/ValidatingTextField.java
new file mode 100644
index 0000000..bb7bf86
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/valid/ValidatingTextField.java
@@ -0,0 +1,188 @@
+//  Copyright 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.tapestry.contrib.valid;
+
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.valid.IValidator;
+import org.apache.tapestry.valid.StringValidator;
+import org.apache.tapestry.valid.ValidField;
+
+/**
+ *
+ *  Backwards compatible version of the 1.0.7 ValidatingTextField component.
+ * 
+ * <table border=1>
+ * <tr>
+ *    <td>Parameter</td>
+ *    <td>Type</td>
+ *	  <td>Read / Write </td>
+ *    <td>Required</td>
+ *    <td>Default</td>
+ *    <td>Description</td>
+ * </tr>
+ *
+ *  <tr>
+ *    <td>text</td>
+ *    <td>java.lang.String</td>
+ *    <td>R / W</td>
+ *   	<td>yes</td>
+ *		<td>&nbsp;</td>
+ *		<td>The text inside the text field.
+ *
+ *  <p>When the form is submitted, the binding is only updated if the value
+ *  is valid.</td>
+ *	</tr>
+ *
+ *  <tr>
+ *      <td>minimumLength</td>
+ *      <td>int</td>
+ *      <td>R</td>
+ *      <td>no</td>
+ *      <td>0</td>
+ *      <td>The minimum length (number of characters read) for the field.  The
+ *  value provided in the request is trimmed of leading and trailing whitespace.
+ *
+ *  <p>If a field is not required and no value is given, then minimumLength is ignored.
+ *  Minimum length only applies if <em>some</em> non-null value is given.</td>
+ *  </tr>
+ *
+ *  <tr>
+ *      <td>required</td>
+ *      <td>boolean</td>
+ *      <td>R</td>
+ *      <td>no</td>
+ *      <td>false</td>
+ *      <td>If true, then a non-null value must be provided.  A value consisting
+ *  only of whitespace is considered null. </td>
+ *  </tr>
+ *
+ *  <tr>
+ *      <td>displayName</td>
+ *      <td>String</td>
+ *      <td>R</td>
+ *      <td>yes</td>
+ *      <td>&nbsp;</td>
+ *      <td>A textual name for the field that is used when formulating error messages.
+ *      </td>
+ *  </tr>
+ *
+ *	</table>
+ *
+ *  <p>May not have a body.  May have informal parameters.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.8
+ *  @see ValidField
+ * 
+ **/
+
+public abstract class ValidatingTextField extends ValidField
+{
+	private IBinding minimumLengthBinding;
+	private IBinding requiredBinding;
+	private IBinding valueBinding;
+
+	/* (non-Javadoc)
+	 * @see org.apache.tapestry.valid.ValidField#getValue()
+	 */
+	public Object getValue()
+	{
+		if (getTextBinding() != null)
+		{
+			return getTextBinding().getObject();
+		}
+		return null;
+	}
+
+	/* (non-Javadoc)
+	 * @see org.apache.tapestry.valid.ValidField#setValue(java.lang.Object)
+	 */
+	public void setValue(Object value)
+	{
+		if(getTextBinding() != null) {
+			getTextBinding().setObject(value);
+		}			
+		// otherwise do nothing, we have nowhere to bind the value to
+	}
+
+	public IBinding getValueBinding()
+	{
+		return valueBinding;
+	}
+
+	public void setValueBinding(IBinding binding)
+	{
+		valueBinding = binding;
+	}
+
+	/** Returns the valueBinding. **/
+	public IBinding getTextBinding()
+	{
+		return getValueBinding();
+	}
+
+	/** Updates valueBinding. **/
+	public void setTextBinding(IBinding value)
+	{
+		setValueBinding(value);
+	}
+
+	public IBinding getMinimumLengthBinding()
+	{
+		return minimumLengthBinding;
+	}
+
+	public void setMinimumLengthBinding(IBinding value)
+	{
+		minimumLengthBinding = value;
+	}
+
+	public IBinding getRequiredBinding()
+	{
+		return requiredBinding;
+	}
+
+	public void setRequiredBinding(IBinding requiredBinding)
+	{
+		this.requiredBinding = requiredBinding;
+	}
+
+	/**
+	 * Overrides {@link ValidField#getValidator()} to construct
+	 * a validator on the fly.
+	 * 
+	 **/
+	public IValidator getValidator()
+	{
+		StringValidator validator = new StringValidator();
+
+		if (requiredBinding != null)
+		{
+			boolean required = requiredBinding.getBoolean();
+
+			validator.setRequired(required);
+		}
+
+		if (minimumLengthBinding != null)
+		{
+			int minimumLength = minimumLengthBinding.getInt();
+
+			validator.setMinimumLength(minimumLength);
+		}
+
+		return validator;
+	}
+}
\ No newline at end of file
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/valid/ValidatingTextField.jwc b/tapestry-contrib/src/org/apache/tapestry/contrib/valid/ValidatingTextField.jwc
new file mode 100644
index 0000000..b6dce0a
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/valid/ValidatingTextField.jwc
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.contrib.valid.ValidatingTextField" allow-body="no">
+
+  <parameter name="disabled" type="boolean" direction="in"/>  
+  <parameter name="hidden" type="boolean" direction="in"/>
+  <parameter name="displayWidth" type="int" direction="in"/>
+  <parameter name="maximumLength" type="int" direction="in"/>
+
+  <parameter name="text" type="java.lang.String" required="yes" direction="custom"/>
+  <parameter name="displayName" type="java.lang.String" required="yes" direction="auto" />
+  <parameter name="minimumLength" type="java.lang.Integer" direction="custom" />
+  <parameter name="required" direction="custom" />
+ 
+  <reserved-parameter name="type"/>
+  <reserved-parameter name="value"/>
+  
+  <property-specification name="name" type="java.lang.String"/>
+  <property-specification name="form" type="org.apache.tapestry.IForm"/>
+
+</component-specification>
diff --git a/tapestry-contrib/src/org/apache/tapestry/contrib/valid/package.html b/tapestry-contrib/src/org/apache/tapestry/contrib/valid/package.html
new file mode 100644
index 0000000..c01ca74
--- /dev/null
+++ b/tapestry-contrib/src/org/apache/tapestry/contrib/valid/package.html
@@ -0,0 +1,20 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+</head>
+<body>
+
+Backwards compatible versions of Tapestry 1.0.7's validating text fields, 
+built as wrappers
+around the 1.0.8 versions.
+
+<p>All that is necessary to use these without changing existing Pages 
+(built against 1.0.7 or earlier)
+is to add aliases for these components to the 
+application specification.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/examples/DevelopmentEnvironment/build.xml b/tapestry-examples/DevelopmentEnvironment/build.xml
similarity index 100%
rename from examples/DevelopmentEnvironment/build.xml
rename to tapestry-examples/DevelopmentEnvironment/build.xml
diff --git a/examples/DevelopmentEnvironment/config/jetty-dev-env.xml b/tapestry-examples/DevelopmentEnvironment/config/jetty-dev-env.xml
similarity index 100%
rename from examples/DevelopmentEnvironment/config/jetty-dev-env.xml
rename to tapestry-examples/DevelopmentEnvironment/config/jetty-dev-env.xml
diff --git a/examples/DevelopmentEnvironment/config/webdefault.xml b/tapestry-examples/DevelopmentEnvironment/config/webdefault.xml
similarity index 100%
rename from examples/DevelopmentEnvironment/config/webdefault.xml
rename to tapestry-examples/DevelopmentEnvironment/config/webdefault.xml
diff --git a/examples/DevelopmentEnvironment/web/WEB-INF/web.xml b/tapestry-examples/DevelopmentEnvironment/web/WEB-INF/web.xml
similarity index 100%
rename from examples/DevelopmentEnvironment/web/WEB-INF/web.xml
rename to tapestry-examples/DevelopmentEnvironment/web/WEB-INF/web.xml
diff --git a/examples/DevelopmentEnvironment/web/index.html b/tapestry-examples/DevelopmentEnvironment/web/index.html
similarity index 100%
rename from examples/DevelopmentEnvironment/web/index.html
rename to tapestry-examples/DevelopmentEnvironment/web/index.html
diff --git a/examples/Vlib/.cvsignore b/tapestry-examples/Vlib/.cvsignore
similarity index 100%
rename from examples/Vlib/.cvsignore
rename to tapestry-examples/Vlib/.cvsignore
diff --git a/examples/Vlib/build.xml b/tapestry-examples/Vlib/build.xml
similarity index 100%
rename from examples/Vlib/build.xml
rename to tapestry-examples/Vlib/build.xml
diff --git a/examples/Vlib/context/ApplicationUnavailable.html b/tapestry-examples/Vlib/context/ApplicationUnavailable.html
similarity index 100%
rename from examples/Vlib/context/ApplicationUnavailable.html
rename to tapestry-examples/Vlib/context/ApplicationUnavailable.html
diff --git a/examples/Vlib/context/BookMatches.html b/tapestry-examples/Vlib/context/BookMatches.html
similarity index 100%
rename from examples/Vlib/context/BookMatches.html
rename to tapestry-examples/Vlib/context/BookMatches.html
diff --git a/examples/Vlib/context/BorrowedBooks.html b/tapestry-examples/Vlib/context/BorrowedBooks.html
similarity index 100%
rename from examples/Vlib/context/BorrowedBooks.html
rename to tapestry-examples/Vlib/context/BorrowedBooks.html
diff --git a/examples/Vlib/context/ConfirmBookDelete.html b/tapestry-examples/Vlib/context/ConfirmBookDelete.html
similarity index 100%
rename from examples/Vlib/context/ConfirmBookDelete.html
rename to tapestry-examples/Vlib/context/ConfirmBookDelete.html
diff --git a/examples/Vlib/context/EditBook.html b/tapestry-examples/Vlib/context/EditBook.html
similarity index 100%
rename from examples/Vlib/context/EditBook.html
rename to tapestry-examples/Vlib/context/EditBook.html
diff --git a/examples/Vlib/context/EditProfile.html b/tapestry-examples/Vlib/context/EditProfile.html
similarity index 100%
rename from examples/Vlib/context/EditProfile.html
rename to tapestry-examples/Vlib/context/EditProfile.html
diff --git a/examples/Vlib/context/EditPublishers.html b/tapestry-examples/Vlib/context/EditPublishers.html
similarity index 100%
rename from examples/Vlib/context/EditPublishers.html
rename to tapestry-examples/Vlib/context/EditPublishers.html
diff --git a/examples/Vlib/context/EditUsers.html b/tapestry-examples/Vlib/context/EditUsers.html
similarity index 100%
rename from examples/Vlib/context/EditUsers.html
rename to tapestry-examples/Vlib/context/EditUsers.html
diff --git a/examples/Vlib/context/GiveAwayBooks.html b/tapestry-examples/Vlib/context/GiveAwayBooks.html
similarity index 100%
rename from examples/Vlib/context/GiveAwayBooks.html
rename to tapestry-examples/Vlib/context/GiveAwayBooks.html
diff --git a/examples/Vlib/context/Home.html b/tapestry-examples/Vlib/context/Home.html
similarity index 100%
rename from examples/Vlib/context/Home.html
rename to tapestry-examples/Vlib/context/Home.html
diff --git a/examples/Vlib/context/Login.html b/tapestry-examples/Vlib/context/Login.html
similarity index 100%
rename from examples/Vlib/context/Login.html
rename to tapestry-examples/Vlib/context/Login.html
diff --git a/examples/Vlib/context/MyLibrary.html b/tapestry-examples/Vlib/context/MyLibrary.html
similarity index 100%
rename from examples/Vlib/context/MyLibrary.html
rename to tapestry-examples/Vlib/context/MyLibrary.html
diff --git a/examples/Vlib/context/NewBook.html b/tapestry-examples/Vlib/context/NewBook.html
similarity index 100%
rename from examples/Vlib/context/NewBook.html
rename to tapestry-examples/Vlib/context/NewBook.html
diff --git a/examples/Vlib/context/Register.html b/tapestry-examples/Vlib/context/Register.html
similarity index 100%
rename from examples/Vlib/context/Register.html
rename to tapestry-examples/Vlib/context/Register.html
diff --git a/examples/Vlib/context/TransferBooksSelect.html b/tapestry-examples/Vlib/context/TransferBooksSelect.html
similarity index 100%
rename from examples/Vlib/context/TransferBooksSelect.html
rename to tapestry-examples/Vlib/context/TransferBooksSelect.html
diff --git a/examples/Vlib/context/TransferBooksTransfer.html b/tapestry-examples/Vlib/context/TransferBooksTransfer.html
similarity index 100%
rename from examples/Vlib/context/TransferBooksTransfer.html
rename to tapestry-examples/Vlib/context/TransferBooksTransfer.html
diff --git a/examples/Vlib/context/ViewBook.html b/tapestry-examples/Vlib/context/ViewBook.html
similarity index 100%
rename from examples/Vlib/context/ViewBook.html
rename to tapestry-examples/Vlib/context/ViewBook.html
diff --git a/examples/Vlib/context/ViewPerson.html b/tapestry-examples/Vlib/context/ViewPerson.html
similarity index 100%
rename from examples/Vlib/context/ViewPerson.html
rename to tapestry-examples/Vlib/context/ViewPerson.html
diff --git a/examples/Vlib/context/WEB-INF/ApplicationUnavailable.page b/tapestry-examples/Vlib/context/WEB-INF/ApplicationUnavailable.page
similarity index 100%
rename from examples/Vlib/context/WEB-INF/ApplicationUnavailable.page
rename to tapestry-examples/Vlib/context/WEB-INF/ApplicationUnavailable.page
diff --git a/examples/Vlib/context/WEB-INF/BookLink.html b/tapestry-examples/Vlib/context/WEB-INF/BookLink.html
similarity index 100%
rename from examples/Vlib/context/WEB-INF/BookLink.html
rename to tapestry-examples/Vlib/context/WEB-INF/BookLink.html
diff --git a/examples/Vlib/context/WEB-INF/BookLink.jwc b/tapestry-examples/Vlib/context/WEB-INF/BookLink.jwc
similarity index 100%
rename from examples/Vlib/context/WEB-INF/BookLink.jwc
rename to tapestry-examples/Vlib/context/WEB-INF/BookLink.jwc
diff --git a/examples/Vlib/context/WEB-INF/BookMatches.page b/tapestry-examples/Vlib/context/WEB-INF/BookMatches.page
similarity index 100%
rename from examples/Vlib/context/WEB-INF/BookMatches.page
rename to tapestry-examples/Vlib/context/WEB-INF/BookMatches.page
diff --git a/examples/Vlib/context/WEB-INF/BookMatches.properties b/tapestry-examples/Vlib/context/WEB-INF/BookMatches.properties
similarity index 100%
rename from examples/Vlib/context/WEB-INF/BookMatches.properties
rename to tapestry-examples/Vlib/context/WEB-INF/BookMatches.properties
diff --git a/examples/Vlib/context/WEB-INF/Border.html b/tapestry-examples/Vlib/context/WEB-INF/Border.html
similarity index 100%
rename from examples/Vlib/context/WEB-INF/Border.html
rename to tapestry-examples/Vlib/context/WEB-INF/Border.html
diff --git a/examples/Vlib/context/WEB-INF/Border.jwc b/tapestry-examples/Vlib/context/WEB-INF/Border.jwc
similarity index 100%
rename from examples/Vlib/context/WEB-INF/Border.jwc
rename to tapestry-examples/Vlib/context/WEB-INF/Border.jwc
diff --git a/examples/Vlib/context/WEB-INF/Border.properties b/tapestry-examples/Vlib/context/WEB-INF/Border.properties
similarity index 100%
rename from examples/Vlib/context/WEB-INF/Border.properties
rename to tapestry-examples/Vlib/context/WEB-INF/Border.properties
diff --git a/examples/Vlib/context/WEB-INF/Borrow.html b/tapestry-examples/Vlib/context/WEB-INF/Borrow.html
similarity index 100%
rename from examples/Vlib/context/WEB-INF/Borrow.html
rename to tapestry-examples/Vlib/context/WEB-INF/Borrow.html
diff --git a/examples/Vlib/context/WEB-INF/Borrow.jwc b/tapestry-examples/Vlib/context/WEB-INF/Borrow.jwc
similarity index 100%
rename from examples/Vlib/context/WEB-INF/Borrow.jwc
rename to tapestry-examples/Vlib/context/WEB-INF/Borrow.jwc
diff --git a/examples/Vlib/context/WEB-INF/BorrowedBooks.page b/tapestry-examples/Vlib/context/WEB-INF/BorrowedBooks.page
similarity index 100%
rename from examples/Vlib/context/WEB-INF/BorrowedBooks.page
rename to tapestry-examples/Vlib/context/WEB-INF/BorrowedBooks.page
diff --git a/examples/Vlib/context/WEB-INF/BorrowedBooks.properties b/tapestry-examples/Vlib/context/WEB-INF/BorrowedBooks.properties
similarity index 100%
rename from examples/Vlib/context/WEB-INF/BorrowedBooks.properties
rename to tapestry-examples/Vlib/context/WEB-INF/BorrowedBooks.properties
diff --git a/examples/Vlib/context/WEB-INF/Browser.jwc b/tapestry-examples/Vlib/context/WEB-INF/Browser.jwc
similarity index 100%
rename from examples/Vlib/context/WEB-INF/Browser.jwc
rename to tapestry-examples/Vlib/context/WEB-INF/Browser.jwc
diff --git a/examples/Vlib/context/WEB-INF/ColumnSorter.html b/tapestry-examples/Vlib/context/WEB-INF/ColumnSorter.html
similarity index 100%
rename from examples/Vlib/context/WEB-INF/ColumnSorter.html
rename to tapestry-examples/Vlib/context/WEB-INF/ColumnSorter.html
diff --git a/examples/Vlib/context/WEB-INF/ColumnSorter.jwc b/tapestry-examples/Vlib/context/WEB-INF/ColumnSorter.jwc
similarity index 100%
rename from examples/Vlib/context/WEB-INF/ColumnSorter.jwc
rename to tapestry-examples/Vlib/context/WEB-INF/ColumnSorter.jwc
diff --git a/examples/Vlib/context/WEB-INF/ConfirmBookDelete.page b/tapestry-examples/Vlib/context/WEB-INF/ConfirmBookDelete.page
similarity index 100%
rename from examples/Vlib/context/WEB-INF/ConfirmBookDelete.page
rename to tapestry-examples/Vlib/context/WEB-INF/ConfirmBookDelete.page
diff --git a/examples/Vlib/context/WEB-INF/ConfirmBookDelete.properties b/tapestry-examples/Vlib/context/WEB-INF/ConfirmBookDelete.properties
similarity index 100%
rename from examples/Vlib/context/WEB-INF/ConfirmBookDelete.properties
rename to tapestry-examples/Vlib/context/WEB-INF/ConfirmBookDelete.properties
diff --git a/examples/Vlib/context/WEB-INF/EditBook.page b/tapestry-examples/Vlib/context/WEB-INF/EditBook.page
similarity index 100%
rename from examples/Vlib/context/WEB-INF/EditBook.page
rename to tapestry-examples/Vlib/context/WEB-INF/EditBook.page
diff --git a/examples/Vlib/context/WEB-INF/EditBook.properties b/tapestry-examples/Vlib/context/WEB-INF/EditBook.properties
similarity index 100%
rename from examples/Vlib/context/WEB-INF/EditBook.properties
rename to tapestry-examples/Vlib/context/WEB-INF/EditBook.properties
diff --git a/examples/Vlib/context/WEB-INF/EditProfile.page b/tapestry-examples/Vlib/context/WEB-INF/EditProfile.page
similarity index 100%
rename from examples/Vlib/context/WEB-INF/EditProfile.page
rename to tapestry-examples/Vlib/context/WEB-INF/EditProfile.page
diff --git a/examples/Vlib/context/WEB-INF/EditProfile.properties b/tapestry-examples/Vlib/context/WEB-INF/EditProfile.properties
similarity index 100%
rename from examples/Vlib/context/WEB-INF/EditProfile.properties
rename to tapestry-examples/Vlib/context/WEB-INF/EditProfile.properties
diff --git a/examples/Vlib/context/WEB-INF/EditPublishers.page b/tapestry-examples/Vlib/context/WEB-INF/EditPublishers.page
similarity index 100%
rename from examples/Vlib/context/WEB-INF/EditPublishers.page
rename to tapestry-examples/Vlib/context/WEB-INF/EditPublishers.page
diff --git a/examples/Vlib/context/WEB-INF/EditPublishers.properties b/tapestry-examples/Vlib/context/WEB-INF/EditPublishers.properties
similarity index 100%
rename from examples/Vlib/context/WEB-INF/EditPublishers.properties
rename to tapestry-examples/Vlib/context/WEB-INF/EditPublishers.properties
diff --git a/examples/Vlib/context/WEB-INF/EditUsers.page b/tapestry-examples/Vlib/context/WEB-INF/EditUsers.page
similarity index 100%
rename from examples/Vlib/context/WEB-INF/EditUsers.page
rename to tapestry-examples/Vlib/context/WEB-INF/EditUsers.page
diff --git a/examples/Vlib/context/WEB-INF/EditUsers.properties b/tapestry-examples/Vlib/context/WEB-INF/EditUsers.properties
similarity index 100%
rename from examples/Vlib/context/WEB-INF/EditUsers.properties
rename to tapestry-examples/Vlib/context/WEB-INF/EditUsers.properties
diff --git a/examples/Vlib/context/WEB-INF/GiveAwayBooks.page b/tapestry-examples/Vlib/context/WEB-INF/GiveAwayBooks.page
similarity index 100%
rename from examples/Vlib/context/WEB-INF/GiveAwayBooks.page
rename to tapestry-examples/Vlib/context/WEB-INF/GiveAwayBooks.page
diff --git a/examples/Vlib/context/WEB-INF/GiveAwayBooks.properties b/tapestry-examples/Vlib/context/WEB-INF/GiveAwayBooks.properties
similarity index 100%
rename from examples/Vlib/context/WEB-INF/GiveAwayBooks.properties
rename to tapestry-examples/Vlib/context/WEB-INF/GiveAwayBooks.properties
diff --git a/examples/Vlib/context/WEB-INF/Home.page b/tapestry-examples/Vlib/context/WEB-INF/Home.page
similarity index 100%
rename from examples/Vlib/context/WEB-INF/Home.page
rename to tapestry-examples/Vlib/context/WEB-INF/Home.page
diff --git a/examples/Vlib/context/WEB-INF/Information.html b/tapestry-examples/Vlib/context/WEB-INF/Information.html
similarity index 100%
rename from examples/Vlib/context/WEB-INF/Information.html
rename to tapestry-examples/Vlib/context/WEB-INF/Information.html
diff --git a/examples/Vlib/context/WEB-INF/Information.jwc b/tapestry-examples/Vlib/context/WEB-INF/Information.jwc
similarity index 100%
rename from examples/Vlib/context/WEB-INF/Information.jwc
rename to tapestry-examples/Vlib/context/WEB-INF/Information.jwc
diff --git a/examples/Vlib/context/WEB-INF/Login.page b/tapestry-examples/Vlib/context/WEB-INF/Login.page
similarity index 100%
rename from examples/Vlib/context/WEB-INF/Login.page
rename to tapestry-examples/Vlib/context/WEB-INF/Login.page
diff --git a/examples/Vlib/context/WEB-INF/MyLibrary.page b/tapestry-examples/Vlib/context/WEB-INF/MyLibrary.page
similarity index 100%
rename from examples/Vlib/context/WEB-INF/MyLibrary.page
rename to tapestry-examples/Vlib/context/WEB-INF/MyLibrary.page
diff --git a/examples/Vlib/context/WEB-INF/MyLibrary.properties b/tapestry-examples/Vlib/context/WEB-INF/MyLibrary.properties
similarity index 100%
rename from examples/Vlib/context/WEB-INF/MyLibrary.properties
rename to tapestry-examples/Vlib/context/WEB-INF/MyLibrary.properties
diff --git a/examples/Vlib/context/WEB-INF/NewBook.page b/tapestry-examples/Vlib/context/WEB-INF/NewBook.page
similarity index 100%
rename from examples/Vlib/context/WEB-INF/NewBook.page
rename to tapestry-examples/Vlib/context/WEB-INF/NewBook.page
diff --git a/examples/Vlib/context/WEB-INF/NewBook.properties b/tapestry-examples/Vlib/context/WEB-INF/NewBook.properties
similarity index 100%
rename from examples/Vlib/context/WEB-INF/NewBook.properties
rename to tapestry-examples/Vlib/context/WEB-INF/NewBook.properties
diff --git a/examples/Vlib/context/WEB-INF/PersonLink.html b/tapestry-examples/Vlib/context/WEB-INF/PersonLink.html
similarity index 100%
rename from examples/Vlib/context/WEB-INF/PersonLink.html
rename to tapestry-examples/Vlib/context/WEB-INF/PersonLink.html
diff --git a/examples/Vlib/context/WEB-INF/PersonLink.jwc b/tapestry-examples/Vlib/context/WEB-INF/PersonLink.jwc
similarity index 100%
rename from examples/Vlib/context/WEB-INF/PersonLink.jwc
rename to tapestry-examples/Vlib/context/WEB-INF/PersonLink.jwc
diff --git a/examples/Vlib/context/WEB-INF/Publisher.script b/tapestry-examples/Vlib/context/WEB-INF/Publisher.script
similarity index 100%
rename from examples/Vlib/context/WEB-INF/Publisher.script
rename to tapestry-examples/Vlib/context/WEB-INF/Publisher.script
diff --git a/examples/Vlib/context/WEB-INF/Question.html b/tapestry-examples/Vlib/context/WEB-INF/Question.html
similarity index 100%
rename from examples/Vlib/context/WEB-INF/Question.html
rename to tapestry-examples/Vlib/context/WEB-INF/Question.html
diff --git a/examples/Vlib/context/WEB-INF/Question.jwc b/tapestry-examples/Vlib/context/WEB-INF/Question.jwc
similarity index 100%
rename from examples/Vlib/context/WEB-INF/Question.jwc
rename to tapestry-examples/Vlib/context/WEB-INF/Question.jwc
diff --git a/examples/Vlib/context/WEB-INF/Register.page b/tapestry-examples/Vlib/context/WEB-INF/Register.page
similarity index 100%
rename from examples/Vlib/context/WEB-INF/Register.page
rename to tapestry-examples/Vlib/context/WEB-INF/Register.page
diff --git a/examples/Vlib/context/WEB-INF/Register.properties b/tapestry-examples/Vlib/context/WEB-INF/Register.properties
similarity index 100%
rename from examples/Vlib/context/WEB-INF/Register.properties
rename to tapestry-examples/Vlib/context/WEB-INF/Register.properties
diff --git a/examples/Vlib/context/WEB-INF/ShowError.html b/tapestry-examples/Vlib/context/WEB-INF/ShowError.html
similarity index 100%
rename from examples/Vlib/context/WEB-INF/ShowError.html
rename to tapestry-examples/Vlib/context/WEB-INF/ShowError.html
diff --git a/examples/Vlib/context/WEB-INF/ShowError.jwc b/tapestry-examples/Vlib/context/WEB-INF/ShowError.jwc
similarity index 100%
rename from examples/Vlib/context/WEB-INF/ShowError.jwc
rename to tapestry-examples/Vlib/context/WEB-INF/ShowError.jwc
diff --git a/examples/Vlib/context/WEB-INF/ShowMessage.html b/tapestry-examples/Vlib/context/WEB-INF/ShowMessage.html
similarity index 100%
rename from examples/Vlib/context/WEB-INF/ShowMessage.html
rename to tapestry-examples/Vlib/context/WEB-INF/ShowMessage.html
diff --git a/examples/Vlib/context/WEB-INF/ShowMessage.jwc b/tapestry-examples/Vlib/context/WEB-INF/ShowMessage.jwc
similarity index 100%
rename from examples/Vlib/context/WEB-INF/ShowMessage.jwc
rename to tapestry-examples/Vlib/context/WEB-INF/ShowMessage.jwc
diff --git a/examples/Vlib/context/WEB-INF/ShowValidationError.html b/tapestry-examples/Vlib/context/WEB-INF/ShowValidationError.html
similarity index 100%
rename from examples/Vlib/context/WEB-INF/ShowValidationError.html
rename to tapestry-examples/Vlib/context/WEB-INF/ShowValidationError.html
diff --git a/examples/Vlib/context/WEB-INF/ShowValidationError.jwc b/tapestry-examples/Vlib/context/WEB-INF/ShowValidationError.jwc
similarity index 100%
rename from examples/Vlib/context/WEB-INF/ShowValidationError.jwc
rename to tapestry-examples/Vlib/context/WEB-INF/ShowValidationError.jwc
diff --git a/examples/Vlib/context/WEB-INF/TransferBooksSelect.page b/tapestry-examples/Vlib/context/WEB-INF/TransferBooksSelect.page
similarity index 100%
rename from examples/Vlib/context/WEB-INF/TransferBooksSelect.page
rename to tapestry-examples/Vlib/context/WEB-INF/TransferBooksSelect.page
diff --git a/examples/Vlib/context/WEB-INF/TransferBooksSelect.properties b/tapestry-examples/Vlib/context/WEB-INF/TransferBooksSelect.properties
similarity index 100%
rename from examples/Vlib/context/WEB-INF/TransferBooksSelect.properties
rename to tapestry-examples/Vlib/context/WEB-INF/TransferBooksSelect.properties
diff --git a/examples/Vlib/context/WEB-INF/TransferBooksTransfer.page b/tapestry-examples/Vlib/context/WEB-INF/TransferBooksTransfer.page
similarity index 100%
rename from examples/Vlib/context/WEB-INF/TransferBooksTransfer.page
rename to tapestry-examples/Vlib/context/WEB-INF/TransferBooksTransfer.page
diff --git a/examples/Vlib/context/WEB-INF/TransferBooksTransfer.properties b/tapestry-examples/Vlib/context/WEB-INF/TransferBooksTransfer.properties
similarity index 100%
rename from examples/Vlib/context/WEB-INF/TransferBooksTransfer.properties
rename to tapestry-examples/Vlib/context/WEB-INF/TransferBooksTransfer.properties
diff --git a/examples/Vlib/context/WEB-INF/ViewBook.page b/tapestry-examples/Vlib/context/WEB-INF/ViewBook.page
similarity index 100%
rename from examples/Vlib/context/WEB-INF/ViewBook.page
rename to tapestry-examples/Vlib/context/WEB-INF/ViewBook.page
diff --git a/examples/Vlib/context/WEB-INF/ViewPerson.page b/tapestry-examples/Vlib/context/WEB-INF/ViewPerson.page
similarity index 100%
rename from examples/Vlib/context/WEB-INF/ViewPerson.page
rename to tapestry-examples/Vlib/context/WEB-INF/ViewPerson.page
diff --git a/examples/Vlib/context/WEB-INF/vlib.application b/tapestry-examples/Vlib/context/WEB-INF/vlib.application
similarity index 100%
rename from examples/Vlib/context/WEB-INF/vlib.application
rename to tapestry-examples/Vlib/context/WEB-INF/vlib.application
diff --git a/examples/Vlib/context/WEB-INF/web.xml b/tapestry-examples/Vlib/context/WEB-INF/web.xml
similarity index 100%
rename from examples/Vlib/context/WEB-INF/web.xml
rename to tapestry-examples/Vlib/context/WEB-INF/web.xml
diff --git a/examples/Vlib/context/css/vlib.css b/tapestry-examples/Vlib/context/css/vlib.css
similarity index 100%
rename from examples/Vlib/context/css/vlib.css
rename to tapestry-examples/Vlib/context/css/vlib.css
diff --git a/examples/Vlib/context/images/.cvsignore b/tapestry-examples/Vlib/context/images/.cvsignore
similarity index 100%
rename from examples/Vlib/context/images/.cvsignore
rename to tapestry-examples/Vlib/context/images/.cvsignore
diff --git a/examples/Vlib/context/images/add.png b/tapestry-examples/Vlib/context/images/add.png
similarity index 100%
rename from examples/Vlib/context/images/add.png
rename to tapestry-examples/Vlib/context/images/add.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser/browser_1x1.png b/tapestry-examples/Vlib/context/images/browser/browser_1x1.png
similarity index 100%
rename from examples/Vlib/context/images/browser/browser_1x1.png
rename to tapestry-examples/Vlib/context/images/browser/browser_1x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser/browser_2x1.png b/tapestry-examples/Vlib/context/images/browser/browser_2x1.png
similarity index 100%
rename from examples/Vlib/context/images/browser/browser_2x1.png
rename to tapestry-examples/Vlib/context/images/browser/browser_2x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser/browser_2x3.png b/tapestry-examples/Vlib/context/images/browser/browser_2x3.png
similarity index 100%
rename from examples/Vlib/context/images/browser/browser_2x3.png
rename to tapestry-examples/Vlib/context/images/browser/browser_2x3.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser/browser_3x1.png b/tapestry-examples/Vlib/context/images/browser/browser_3x1.png
similarity index 100%
rename from examples/Vlib/context/images/browser/browser_3x1.png
rename to tapestry-examples/Vlib/context/images/browser/browser_3x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser/browser_4x1.png b/tapestry-examples/Vlib/context/images/browser/browser_4x1.png
similarity index 100%
rename from examples/Vlib/context/images/browser/browser_4x1.png
rename to tapestry-examples/Vlib/context/images/browser/browser_4x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser/browser_4x2.png b/tapestry-examples/Vlib/context/images/browser/browser_4x2.png
similarity index 100%
rename from examples/Vlib/context/images/browser/browser_4x2.png
rename to tapestry-examples/Vlib/context/images/browser/browser_4x2.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser/browser_4x3.png b/tapestry-examples/Vlib/context/images/browser/browser_4x3.png
similarity index 100%
rename from examples/Vlib/context/images/browser/browser_4x3.png
rename to tapestry-examples/Vlib/context/images/browser/browser_4x3.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser/browser_4x4.png b/tapestry-examples/Vlib/context/images/browser/browser_4x4.png
similarity index 100%
rename from examples/Vlib/context/images/browser/browser_4x4.png
rename to tapestry-examples/Vlib/context/images/browser/browser_4x4.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser/browser_4x5.png b/tapestry-examples/Vlib/context/images/browser/browser_4x5.png
similarity index 100%
rename from examples/Vlib/context/images/browser/browser_4x5.png
rename to tapestry-examples/Vlib/context/images/browser/browser_4x5.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser/browser_4x6.png b/tapestry-examples/Vlib/context/images/browser/browser_4x6.png
similarity index 100%
rename from examples/Vlib/context/images/browser/browser_4x6.png
rename to tapestry-examples/Vlib/context/images/browser/browser_4x6.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser/browser_4x7.png b/tapestry-examples/Vlib/context/images/browser/browser_4x7.png
similarity index 100%
rename from examples/Vlib/context/images/browser/browser_4x7.png
rename to tapestry-examples/Vlib/context/images/browser/browser_4x7.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser/browser_5x1.png b/tapestry-examples/Vlib/context/images/browser/browser_5x1.png
similarity index 100%
rename from examples/Vlib/context/images/browser/browser_5x1.png
rename to tapestry-examples/Vlib/context/images/browser/browser_5x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser_d/browser_4x2.png b/tapestry-examples/Vlib/context/images/browser_d/browser_4x2.png
similarity index 100%
rename from examples/Vlib/context/images/browser_d/browser_4x2.png
rename to tapestry-examples/Vlib/context/images/browser_d/browser_4x2.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser_d/browser_4x3.png b/tapestry-examples/Vlib/context/images/browser_d/browser_4x3.png
similarity index 100%
rename from examples/Vlib/context/images/browser_d/browser_4x3.png
rename to tapestry-examples/Vlib/context/images/browser_d/browser_4x3.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser_d/browser_4x5.png b/tapestry-examples/Vlib/context/images/browser_d/browser_4x5.png
similarity index 100%
rename from examples/Vlib/context/images/browser_d/browser_4x5.png
rename to tapestry-examples/Vlib/context/images/browser_d/browser_4x5.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser_d/browser_4x6.png b/tapestry-examples/Vlib/context/images/browser_d/browser_4x6.png
similarity index 100%
rename from examples/Vlib/context/images/browser_d/browser_4x6.png
rename to tapestry-examples/Vlib/context/images/browser_d/browser_4x6.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser_e/browser_4x2.png b/tapestry-examples/Vlib/context/images/browser_e/browser_4x2.png
similarity index 100%
rename from examples/Vlib/context/images/browser_e/browser_4x2.png
rename to tapestry-examples/Vlib/context/images/browser_e/browser_4x2.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser_e/browser_4x3.png b/tapestry-examples/Vlib/context/images/browser_e/browser_4x3.png
similarity index 100%
rename from examples/Vlib/context/images/browser_e/browser_4x3.png
rename to tapestry-examples/Vlib/context/images/browser_e/browser_4x3.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser_e/browser_4x5.png b/tapestry-examples/Vlib/context/images/browser_e/browser_4x5.png
similarity index 100%
rename from examples/Vlib/context/images/browser_e/browser_4x5.png
rename to tapestry-examples/Vlib/context/images/browser_e/browser_4x5.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser_e/browser_4x6.png b/tapestry-examples/Vlib/context/images/browser_e/browser_4x6.png
similarity index 100%
rename from examples/Vlib/context/images/browser_e/browser_4x6.png
rename to tapestry-examples/Vlib/context/images/browser_e/browser_4x6.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser_h/browser_4x2.png b/tapestry-examples/Vlib/context/images/browser_h/browser_4x2.png
similarity index 100%
rename from examples/Vlib/context/images/browser_h/browser_4x2.png
rename to tapestry-examples/Vlib/context/images/browser_h/browser_4x2.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser_h/browser_4x3.png b/tapestry-examples/Vlib/context/images/browser_h/browser_4x3.png
similarity index 100%
rename from examples/Vlib/context/images/browser_h/browser_4x3.png
rename to tapestry-examples/Vlib/context/images/browser_h/browser_4x3.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser_h/browser_4x5.png b/tapestry-examples/Vlib/context/images/browser_h/browser_4x5.png
similarity index 100%
rename from examples/Vlib/context/images/browser_h/browser_4x5.png
rename to tapestry-examples/Vlib/context/images/browser_h/browser_4x5.png
Binary files differ
diff --git a/examples/Vlib/context/images/browser_h/browser_4x6.png b/tapestry-examples/Vlib/context/images/browser_h/browser_4x6.png
similarity index 100%
rename from examples/Vlib/context/images/browser_h/browser_4x6.png
rename to tapestry-examples/Vlib/context/images/browser_h/browser_4x6.png
Binary files differ
diff --git a/examples/Vlib/context/images/cancel.png b/tapestry-examples/Vlib/context/images/cancel.png
similarity index 100%
rename from examples/Vlib/context/images/cancel.png
rename to tapestry-examples/Vlib/context/images/cancel.png
Binary files differ
diff --git a/examples/Vlib/context/images/checkout.png b/tapestry-examples/Vlib/context/images/checkout.png
similarity index 100%
rename from examples/Vlib/context/images/checkout.png
rename to tapestry-examples/Vlib/context/images/checkout.png
Binary files differ
diff --git a/examples/Vlib/context/images/checkout_h.png b/tapestry-examples/Vlib/context/images/checkout_h.png
similarity index 100%
rename from examples/Vlib/context/images/checkout_h.png
rename to tapestry-examples/Vlib/context/images/checkout_h.png
Binary files differ
diff --git a/examples/Vlib/context/images/continue.png b/tapestry-examples/Vlib/context/images/continue.png
similarity index 100%
rename from examples/Vlib/context/images/continue.png
rename to tapestry-examples/Vlib/context/images/continue.png
Binary files differ
diff --git a/examples/Vlib/context/images/delete-cancel.png b/tapestry-examples/Vlib/context/images/delete-cancel.png
similarity index 100%
rename from examples/Vlib/context/images/delete-cancel.png
rename to tapestry-examples/Vlib/context/images/delete-cancel.png
Binary files differ
diff --git a/examples/Vlib/context/images/delete-cancel_h.png b/tapestry-examples/Vlib/context/images/delete-cancel_h.png
similarity index 100%
rename from examples/Vlib/context/images/delete-cancel_h.png
rename to tapestry-examples/Vlib/context/images/delete-cancel_h.png
Binary files differ
diff --git a/examples/Vlib/context/images/delete-confirm.png b/tapestry-examples/Vlib/context/images/delete-confirm.png
similarity index 100%
rename from examples/Vlib/context/images/delete-confirm.png
rename to tapestry-examples/Vlib/context/images/delete-confirm.png
Binary files differ
diff --git a/examples/Vlib/context/images/delete-confirm_h.png b/tapestry-examples/Vlib/context/images/delete-confirm_h.png
similarity index 100%
rename from examples/Vlib/context/images/delete-confirm_h.png
rename to tapestry-examples/Vlib/context/images/delete-confirm_h.png
Binary files differ
diff --git a/examples/Vlib/context/images/delete.png b/tapestry-examples/Vlib/context/images/delete.png
similarity index 100%
rename from examples/Vlib/context/images/delete.png
rename to tapestry-examples/Vlib/context/images/delete.png
Binary files differ
diff --git a/examples/Vlib/context/images/delete_h.png b/tapestry-examples/Vlib/context/images/delete_h.png
similarity index 100%
rename from examples/Vlib/context/images/delete_h.png
rename to tapestry-examples/Vlib/context/images/delete_h.png
Binary files differ
diff --git a/examples/Vlib/context/images/edit.png b/tapestry-examples/Vlib/context/images/edit.png
similarity index 100%
rename from examples/Vlib/context/images/edit.png
rename to tapestry-examples/Vlib/context/images/edit.png
Binary files differ
diff --git a/examples/Vlib/context/images/edit_h.png b/tapestry-examples/Vlib/context/images/edit_h.png
similarity index 100%
rename from examples/Vlib/context/images/edit_h.png
rename to tapestry-examples/Vlib/context/images/edit_h.png
Binary files differ
diff --git a/examples/Vlib/context/images/error-icon.png b/tapestry-examples/Vlib/context/images/error-icon.png
similarity index 100%
rename from examples/Vlib/context/images/error-icon.png
rename to tapestry-examples/Vlib/context/images/error-icon.png
Binary files differ
diff --git a/examples/Vlib/context/images/info-icon.png b/tapestry-examples/Vlib/context/images/info-icon.png
similarity index 100%
rename from examples/Vlib/context/images/info-icon.png
rename to tapestry-examples/Vlib/context/images/info-icon.png
Binary files differ
diff --git a/examples/Vlib/context/images/login.png b/tapestry-examples/Vlib/context/images/login.png
similarity index 100%
rename from examples/Vlib/context/images/login.png
rename to tapestry-examples/Vlib/context/images/login.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-h/nav_10x1.png b/tapestry-examples/Vlib/context/images/nav-h/nav_10x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-h/nav_10x1.png
rename to tapestry-examples/Vlib/context/images/nav-h/nav_10x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-h/nav_10x1_login.png b/tapestry-examples/Vlib/context/images/nav-h/nav_10x1_login.png
similarity index 100%
rename from examples/Vlib/context/images/nav-h/nav_10x1_login.png
rename to tapestry-examples/Vlib/context/images/nav-h/nav_10x1_login.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-h/nav_1x1.png b/tapestry-examples/Vlib/context/images/nav-h/nav_1x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-h/nav_1x1.png
rename to tapestry-examples/Vlib/context/images/nav-h/nav_1x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-h/nav_2x1.png b/tapestry-examples/Vlib/context/images/nav-h/nav_2x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-h/nav_2x1.png
rename to tapestry-examples/Vlib/context/images/nav-h/nav_2x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-h/nav_3x1.png b/tapestry-examples/Vlib/context/images/nav-h/nav_3x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-h/nav_3x1.png
rename to tapestry-examples/Vlib/context/images/nav-h/nav_3x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-h/nav_4x1.png b/tapestry-examples/Vlib/context/images/nav-h/nav_4x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-h/nav_4x1.png
rename to tapestry-examples/Vlib/context/images/nav-h/nav_4x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-h/nav_5x1.png b/tapestry-examples/Vlib/context/images/nav-h/nav_5x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-h/nav_5x1.png
rename to tapestry-examples/Vlib/context/images/nav-h/nav_5x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-h/nav_5x1_editprofile.png b/tapestry-examples/Vlib/context/images/nav-h/nav_5x1_editprofile.png
similarity index 100%
rename from examples/Vlib/context/images/nav-h/nav_5x1_editprofile.png
rename to tapestry-examples/Vlib/context/images/nav-h/nav_5x1_editprofile.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-h/nav_7x1.png b/tapestry-examples/Vlib/context/images/nav-h/nav_7x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-h/nav_7x1.png
rename to tapestry-examples/Vlib/context/images/nav-h/nav_7x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-h/nav_8x1.png b/tapestry-examples/Vlib/context/images/nav-h/nav_8x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-h/nav_8x1.png
rename to tapestry-examples/Vlib/context/images/nav-h/nav_8x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-h/nav_9x1.png b/tapestry-examples/Vlib/context/images/nav-h/nav_9x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-h/nav_9x1.png
rename to tapestry-examples/Vlib/context/images/nav-h/nav_9x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-selected-h/nav_10x1_login.png b/tapestry-examples/Vlib/context/images/nav-selected-h/nav_10x1_login.png
similarity index 100%
rename from examples/Vlib/context/images/nav-selected-h/nav_10x1_login.png
rename to tapestry-examples/Vlib/context/images/nav-selected-h/nav_10x1_login.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-selected-h/nav_1x1.png b/tapestry-examples/Vlib/context/images/nav-selected-h/nav_1x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-selected-h/nav_1x1.png
rename to tapestry-examples/Vlib/context/images/nav-selected-h/nav_1x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-selected-h/nav_2x1.png b/tapestry-examples/Vlib/context/images/nav-selected-h/nav_2x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-selected-h/nav_2x1.png
rename to tapestry-examples/Vlib/context/images/nav-selected-h/nav_2x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-selected-h/nav_3x1.png b/tapestry-examples/Vlib/context/images/nav-selected-h/nav_3x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-selected-h/nav_3x1.png
rename to tapestry-examples/Vlib/context/images/nav-selected-h/nav_3x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-selected-h/nav_4x1.png b/tapestry-examples/Vlib/context/images/nav-selected-h/nav_4x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-selected-h/nav_4x1.png
rename to tapestry-examples/Vlib/context/images/nav-selected-h/nav_4x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-selected-h/nav_5x1.png b/tapestry-examples/Vlib/context/images/nav-selected-h/nav_5x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-selected-h/nav_5x1.png
rename to tapestry-examples/Vlib/context/images/nav-selected-h/nav_5x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-selected-h/nav_5x1_editprofile.png b/tapestry-examples/Vlib/context/images/nav-selected-h/nav_5x1_editprofile.png
similarity index 100%
rename from examples/Vlib/context/images/nav-selected-h/nav_5x1_editprofile.png
rename to tapestry-examples/Vlib/context/images/nav-selected-h/nav_5x1_editprofile.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-selected-h/nav_7x1.png b/tapestry-examples/Vlib/context/images/nav-selected-h/nav_7x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-selected-h/nav_7x1.png
rename to tapestry-examples/Vlib/context/images/nav-selected-h/nav_7x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-selected-h/nav_8x1.png b/tapestry-examples/Vlib/context/images/nav-selected-h/nav_8x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-selected-h/nav_8x1.png
rename to tapestry-examples/Vlib/context/images/nav-selected-h/nav_8x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-selected-h/nav_9x1.png b/tapestry-examples/Vlib/context/images/nav-selected-h/nav_9x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-selected-h/nav_9x1.png
rename to tapestry-examples/Vlib/context/images/nav-selected-h/nav_9x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-selected/nav_10x1_login.png b/tapestry-examples/Vlib/context/images/nav-selected/nav_10x1_login.png
similarity index 100%
rename from examples/Vlib/context/images/nav-selected/nav_10x1_login.png
rename to tapestry-examples/Vlib/context/images/nav-selected/nav_10x1_login.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-selected/nav_1x1.png b/tapestry-examples/Vlib/context/images/nav-selected/nav_1x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-selected/nav_1x1.png
rename to tapestry-examples/Vlib/context/images/nav-selected/nav_1x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-selected/nav_2x1.png b/tapestry-examples/Vlib/context/images/nav-selected/nav_2x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-selected/nav_2x1.png
rename to tapestry-examples/Vlib/context/images/nav-selected/nav_2x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-selected/nav_3x1.png b/tapestry-examples/Vlib/context/images/nav-selected/nav_3x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-selected/nav_3x1.png
rename to tapestry-examples/Vlib/context/images/nav-selected/nav_3x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-selected/nav_4x1.png b/tapestry-examples/Vlib/context/images/nav-selected/nav_4x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-selected/nav_4x1.png
rename to tapestry-examples/Vlib/context/images/nav-selected/nav_4x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-selected/nav_5x1.png b/tapestry-examples/Vlib/context/images/nav-selected/nav_5x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-selected/nav_5x1.png
rename to tapestry-examples/Vlib/context/images/nav-selected/nav_5x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-selected/nav_5x1_editprofile.png b/tapestry-examples/Vlib/context/images/nav-selected/nav_5x1_editprofile.png
similarity index 100%
rename from examples/Vlib/context/images/nav-selected/nav_5x1_editprofile.png
rename to tapestry-examples/Vlib/context/images/nav-selected/nav_5x1_editprofile.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-selected/nav_7x1.png b/tapestry-examples/Vlib/context/images/nav-selected/nav_7x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-selected/nav_7x1.png
rename to tapestry-examples/Vlib/context/images/nav-selected/nav_7x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-selected/nav_8x1.png b/tapestry-examples/Vlib/context/images/nav-selected/nav_8x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-selected/nav_8x1.png
rename to tapestry-examples/Vlib/context/images/nav-selected/nav_8x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav-selected/nav_9x1.png b/tapestry-examples/Vlib/context/images/nav-selected/nav_9x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav-selected/nav_9x1.png
rename to tapestry-examples/Vlib/context/images/nav-selected/nav_9x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav/nav_10x1.png b/tapestry-examples/Vlib/context/images/nav/nav_10x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav/nav_10x1.png
rename to tapestry-examples/Vlib/context/images/nav/nav_10x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav/nav_10x1_login.png b/tapestry-examples/Vlib/context/images/nav/nav_10x1_login.png
similarity index 100%
rename from examples/Vlib/context/images/nav/nav_10x1_login.png
rename to tapestry-examples/Vlib/context/images/nav/nav_10x1_login.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav/nav_1x1.png b/tapestry-examples/Vlib/context/images/nav/nav_1x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav/nav_1x1.png
rename to tapestry-examples/Vlib/context/images/nav/nav_1x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav/nav_2x1.png b/tapestry-examples/Vlib/context/images/nav/nav_2x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav/nav_2x1.png
rename to tapestry-examples/Vlib/context/images/nav/nav_2x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav/nav_3x1.png b/tapestry-examples/Vlib/context/images/nav/nav_3x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav/nav_3x1.png
rename to tapestry-examples/Vlib/context/images/nav/nav_3x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav/nav_4x1.png b/tapestry-examples/Vlib/context/images/nav/nav_4x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav/nav_4x1.png
rename to tapestry-examples/Vlib/context/images/nav/nav_4x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav/nav_5x1.png b/tapestry-examples/Vlib/context/images/nav/nav_5x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav/nav_5x1.png
rename to tapestry-examples/Vlib/context/images/nav/nav_5x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav/nav_5x1_editprofile.png b/tapestry-examples/Vlib/context/images/nav/nav_5x1_editprofile.png
similarity index 100%
rename from examples/Vlib/context/images/nav/nav_5x1_editprofile.png
rename to tapestry-examples/Vlib/context/images/nav/nav_5x1_editprofile.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav/nav_6x1.png b/tapestry-examples/Vlib/context/images/nav/nav_6x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav/nav_6x1.png
rename to tapestry-examples/Vlib/context/images/nav/nav_6x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav/nav_7x1.png b/tapestry-examples/Vlib/context/images/nav/nav_7x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav/nav_7x1.png
rename to tapestry-examples/Vlib/context/images/nav/nav_7x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav/nav_8x1.png b/tapestry-examples/Vlib/context/images/nav/nav_8x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav/nav_8x1.png
rename to tapestry-examples/Vlib/context/images/nav/nav_8x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/nav/nav_9x1.png b/tapestry-examples/Vlib/context/images/nav/nav_9x1.png
similarity index 100%
rename from examples/Vlib/context/images/nav/nav_9x1.png
rename to tapestry-examples/Vlib/context/images/nav/nav_9x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/new.png b/tapestry-examples/Vlib/context/images/new.png
similarity index 100%
rename from examples/Vlib/context/images/new.png
rename to tapestry-examples/Vlib/context/images/new.png
Binary files differ
diff --git a/examples/Vlib/context/images/question-icon.png b/tapestry-examples/Vlib/context/images/question-icon.png
similarity index 100%
rename from examples/Vlib/context/images/question-icon.png
rename to tapestry-examples/Vlib/context/images/question-icon.png
Binary files differ
diff --git a/examples/Vlib/context/images/register.png b/tapestry-examples/Vlib/context/images/register.png
similarity index 100%
rename from examples/Vlib/context/images/register.png
rename to tapestry-examples/Vlib/context/images/register.png
Binary files differ
diff --git a/examples/Vlib/context/images/return.png b/tapestry-examples/Vlib/context/images/return.png
similarity index 100%
rename from examples/Vlib/context/images/return.png
rename to tapestry-examples/Vlib/context/images/return.png
Binary files differ
diff --git a/examples/Vlib/context/images/return_h.png b/tapestry-examples/Vlib/context/images/return_h.png
similarity index 100%
rename from examples/Vlib/context/images/return_h.png
rename to tapestry-examples/Vlib/context/images/return_h.png
Binary files differ
diff --git a/examples/Vlib/context/images/search.png b/tapestry-examples/Vlib/context/images/search.png
similarity index 100%
rename from examples/Vlib/context/images/search.png
rename to tapestry-examples/Vlib/context/images/search.png
Binary files differ
diff --git a/examples/Vlib/context/images/sort-down.png b/tapestry-examples/Vlib/context/images/sort-down.png
similarity index 100%
rename from examples/Vlib/context/images/sort-down.png
rename to tapestry-examples/Vlib/context/images/sort-down.png
Binary files differ
diff --git a/examples/Vlib/context/images/sort-down_h.png b/tapestry-examples/Vlib/context/images/sort-down_h.png
similarity index 100%
rename from examples/Vlib/context/images/sort-down_h.png
rename to tapestry-examples/Vlib/context/images/sort-down_h.png
Binary files differ
diff --git a/examples/Vlib/context/images/sort-up.png b/tapestry-examples/Vlib/context/images/sort-up.png
similarity index 100%
rename from examples/Vlib/context/images/sort-up.png
rename to tapestry-examples/Vlib/context/images/sort-up.png
Binary files differ
diff --git a/examples/Vlib/context/images/sort-up_h.png b/tapestry-examples/Vlib/context/images/sort-up_h.png
similarity index 100%
rename from examples/Vlib/context/images/sort-up_h.png
rename to tapestry-examples/Vlib/context/images/sort-up_h.png
Binary files differ
diff --git a/examples/Vlib/context/images/spacer.png b/tapestry-examples/Vlib/context/images/spacer.png
similarity index 100%
rename from examples/Vlib/context/images/spacer.png
rename to tapestry-examples/Vlib/context/images/spacer.png
Binary files differ
diff --git a/examples/Vlib/context/images/step1.png b/tapestry-examples/Vlib/context/images/step1.png
similarity index 100%
rename from examples/Vlib/context/images/step1.png
rename to tapestry-examples/Vlib/context/images/step1.png
Binary files differ
diff --git a/examples/Vlib/context/images/step2.png b/tapestry-examples/Vlib/context/images/step2.png
similarity index 100%
rename from examples/Vlib/context/images/step2.png
rename to tapestry-examples/Vlib/context/images/step2.png
Binary files differ
diff --git a/examples/Vlib/context/images/title/AddNewBook.png b/tapestry-examples/Vlib/context/images/title/AddNewBook.png
similarity index 100%
rename from examples/Vlib/context/images/title/AddNewBook.png
rename to tapestry-examples/Vlib/context/images/title/AddNewBook.png
Binary files differ
diff --git a/examples/Vlib/context/images/title/BookMatches.png b/tapestry-examples/Vlib/context/images/title/BookMatches.png
similarity index 100%
rename from examples/Vlib/context/images/title/BookMatches.png
rename to tapestry-examples/Vlib/context/images/title/BookMatches.png
Binary files differ
diff --git a/examples/Vlib/context/images/title/BorrowedBooks.png b/tapestry-examples/Vlib/context/images/title/BorrowedBooks.png
similarity index 100%
rename from examples/Vlib/context/images/title/BorrowedBooks.png
rename to tapestry-examples/Vlib/context/images/title/BorrowedBooks.png
Binary files differ
diff --git a/examples/Vlib/context/images/title/DeleteBook.png b/tapestry-examples/Vlib/context/images/title/DeleteBook.png
similarity index 100%
rename from examples/Vlib/context/images/title/DeleteBook.png
rename to tapestry-examples/Vlib/context/images/title/DeleteBook.png
Binary files differ
diff --git a/examples/Vlib/context/images/title/EditBook.png b/tapestry-examples/Vlib/context/images/title/EditBook.png
similarity index 100%
rename from examples/Vlib/context/images/title/EditBook.png
rename to tapestry-examples/Vlib/context/images/title/EditBook.png
Binary files differ
diff --git a/examples/Vlib/context/images/title/EditProfile.png b/tapestry-examples/Vlib/context/images/title/EditProfile.png
similarity index 100%
rename from examples/Vlib/context/images/title/EditProfile.png
rename to tapestry-examples/Vlib/context/images/title/EditProfile.png
Binary files differ
diff --git a/examples/Vlib/context/images/title/EditPublishers.png b/tapestry-examples/Vlib/context/images/title/EditPublishers.png
similarity index 100%
rename from examples/Vlib/context/images/title/EditPublishers.png
rename to tapestry-examples/Vlib/context/images/title/EditPublishers.png
Binary files differ
diff --git a/examples/Vlib/context/images/title/EditUsers.png b/tapestry-examples/Vlib/context/images/title/EditUsers.png
similarity index 100%
rename from examples/Vlib/context/images/title/EditUsers.png
rename to tapestry-examples/Vlib/context/images/title/EditUsers.png
Binary files differ
diff --git a/examples/Vlib/context/images/title/GiveAwayBooks.png b/tapestry-examples/Vlib/context/images/title/GiveAwayBooks.png
similarity index 100%
rename from examples/Vlib/context/images/title/GiveAwayBooks.png
rename to tapestry-examples/Vlib/context/images/title/GiveAwayBooks.png
Binary files differ
diff --git a/examples/Vlib/context/images/title/Login.png b/tapestry-examples/Vlib/context/images/title/Login.png
similarity index 100%
rename from examples/Vlib/context/images/title/Login.png
rename to tapestry-examples/Vlib/context/images/title/Login.png
Binary files differ
diff --git a/examples/Vlib/context/images/title/MyLibrary.png b/tapestry-examples/Vlib/context/images/title/MyLibrary.png
similarity index 100%
rename from examples/Vlib/context/images/title/MyLibrary.png
rename to tapestry-examples/Vlib/context/images/title/MyLibrary.png
Binary files differ
diff --git a/examples/Vlib/context/images/title/Register.png b/tapestry-examples/Vlib/context/images/title/Register.png
similarity index 100%
rename from examples/Vlib/context/images/title/Register.png
rename to tapestry-examples/Vlib/context/images/title/Register.png
Binary files differ
diff --git a/examples/Vlib/context/images/title/Search.png b/tapestry-examples/Vlib/context/images/title/Search.png
similarity index 100%
rename from examples/Vlib/context/images/title/Search.png
rename to tapestry-examples/Vlib/context/images/title/Search.png
Binary files differ
diff --git a/examples/Vlib/context/images/title/TransferBooks.png b/tapestry-examples/Vlib/context/images/title/TransferBooks.png
similarity index 100%
rename from examples/Vlib/context/images/title/TransferBooks.png
rename to tapestry-examples/Vlib/context/images/title/TransferBooks.png
Binary files differ
diff --git a/examples/Vlib/context/images/title/ViewBook.png b/tapestry-examples/Vlib/context/images/title/ViewBook.png
similarity index 100%
rename from examples/Vlib/context/images/title/ViewBook.png
rename to tapestry-examples/Vlib/context/images/title/ViewBook.png
Binary files differ
diff --git a/examples/Vlib/context/images/title/ViewPerson.png b/tapestry-examples/Vlib/context/images/title/ViewPerson.png
similarity index 100%
rename from examples/Vlib/context/images/title/ViewPerson.png
rename to tapestry-examples/Vlib/context/images/title/ViewPerson.png
Binary files differ
diff --git a/examples/Vlib/context/images/title/blank.png b/tapestry-examples/Vlib/context/images/title/blank.png
similarity index 100%
rename from examples/Vlib/context/images/title/blank.png
rename to tapestry-examples/Vlib/context/images/title/blank.png
Binary files differ
diff --git a/examples/Vlib/context/images/title/title_1x1.png b/tapestry-examples/Vlib/context/images/title/title_1x1.png
similarity index 100%
rename from examples/Vlib/context/images/title/title_1x1.png
rename to tapestry-examples/Vlib/context/images/title/title_1x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/title/title_2x1.png b/tapestry-examples/Vlib/context/images/title/title_2x1.png
similarity index 100%
rename from examples/Vlib/context/images/title/title_2x1.png
rename to tapestry-examples/Vlib/context/images/title/title_2x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/title/title_2x3.png b/tapestry-examples/Vlib/context/images/title/title_2x3.png
similarity index 100%
rename from examples/Vlib/context/images/title/title_2x3.png
rename to tapestry-examples/Vlib/context/images/title/title_2x3.png
Binary files differ
diff --git a/examples/Vlib/context/images/title/title_3x1.png b/tapestry-examples/Vlib/context/images/title/title_3x1.png
similarity index 100%
rename from examples/Vlib/context/images/title/title_3x1.png
rename to tapestry-examples/Vlib/context/images/title/title_3x1.png
Binary files differ
diff --git a/examples/Vlib/context/images/transfer.png b/tapestry-examples/Vlib/context/images/transfer.png
similarity index 100%
rename from examples/Vlib/context/images/transfer.png
rename to tapestry-examples/Vlib/context/images/transfer.png
Binary files differ
diff --git a/examples/Vlib/context/images/update.png b/tapestry-examples/Vlib/context/images/update.png
similarity index 100%
rename from examples/Vlib/context/images/update.png
rename to tapestry-examples/Vlib/context/images/update.png
Binary files differ
diff --git a/examples/Vlib/jetty.xml b/tapestry-examples/Vlib/jetty.xml
similarity index 100%
rename from examples/Vlib/jetty.xml
rename to tapestry-examples/Vlib/jetty.xml
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/ActivateCallback.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/ActivateCallback.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/ActivateCallback.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/ActivateCallback.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/ActivatePage.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/ActivatePage.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/ActivatePage.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/ActivatePage.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/AdminPage.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/AdminPage.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/AdminPage.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/AdminPage.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/EntitySelectionModel.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/EntitySelectionModel.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/EntitySelectionModel.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/EntitySelectionModel.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/Global.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/Global.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/Global.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/Global.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/IActivate.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/IActivate.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/IActivate.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/IActivate.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/IErrorProperty.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/IErrorProperty.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/IErrorProperty.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/IErrorProperty.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/IMessageProperty.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/IMessageProperty.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/IMessageProperty.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/IMessageProperty.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/Protected.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/Protected.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/Protected.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/Protected.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/VirtualLibraryDelegate.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/VirtualLibraryDelegate.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/VirtualLibraryDelegate.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/VirtualLibraryDelegate.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/VirtualLibraryEngine.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/VirtualLibraryEngine.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/VirtualLibraryEngine.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/VirtualLibraryEngine.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/Visit.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/Visit.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/Visit.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/Visit.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/components/BookLink.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/components/BookLink.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/components/BookLink.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/components/BookLink.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/components/Border.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/components/Border.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/components/Border.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/components/Border.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/components/Borrow.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/components/Borrow.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/components/Borrow.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/components/Borrow.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/components/Browser.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/components/Browser.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/components/Browser.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/components/Browser.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/components/ColumnSorter.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/components/ColumnSorter.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/components/ColumnSorter.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/components/ColumnSorter.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/pages/ApplicationUnavailable.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/ApplicationUnavailable.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/pages/ApplicationUnavailable.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/ApplicationUnavailable.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/pages/BookMatches.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/BookMatches.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/pages/BookMatches.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/BookMatches.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/pages/BorrowedBooks.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/BorrowedBooks.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/pages/BorrowedBooks.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/BorrowedBooks.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/pages/ConfirmBookDelete.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/ConfirmBookDelete.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/pages/ConfirmBookDelete.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/ConfirmBookDelete.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/pages/EditBook.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/EditBook.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/pages/EditBook.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/EditBook.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/pages/EditProfile.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/EditProfile.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/pages/EditProfile.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/EditProfile.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/pages/GiveAwayBooks.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/GiveAwayBooks.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/pages/GiveAwayBooks.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/GiveAwayBooks.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/pages/Home.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/Home.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/pages/Home.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/Home.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/pages/Login.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/Login.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/pages/Login.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/Login.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/pages/MyLibrary.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/MyLibrary.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/pages/MyLibrary.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/MyLibrary.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/pages/NewBook.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/NewBook.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/pages/NewBook.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/NewBook.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/pages/Register.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/Register.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/pages/Register.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/Register.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/pages/ViewBook.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/ViewBook.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/pages/ViewBook.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/ViewBook.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/pages/ViewPerson.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/ViewPerson.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/pages/ViewPerson.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/ViewPerson.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/pages/admin/EditPublishers.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/admin/EditPublishers.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/pages/admin/EditPublishers.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/admin/EditPublishers.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/pages/admin/EditUsers.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/admin/EditUsers.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/pages/admin/EditUsers.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/admin/EditUsers.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/pages/admin/TransferBooksSelect.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/admin/TransferBooksSelect.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/pages/admin/TransferBooksSelect.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/admin/TransferBooksSelect.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/pages/admin/TransferBooksTransfer.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/admin/TransferBooksTransfer.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/pages/admin/TransferBooksTransfer.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/admin/TransferBooksTransfer.java
diff --git a/examples/Vlib/src/org/apache/tapestry/vlib/pages/admin/UserListEditMap.java b/tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/admin/UserListEditMap.java
similarity index 100%
rename from examples/Vlib/src/org/apache/tapestry/vlib/pages/admin/UserListEditMap.java
rename to tapestry-examples/Vlib/src/org/apache/tapestry/vlib/pages/admin/UserListEditMap.java
diff --git a/examples/VlibBeans/.cvsignore b/tapestry-examples/VlibBeans/.cvsignore
similarity index 100%
rename from examples/VlibBeans/.cvsignore
rename to tapestry-examples/VlibBeans/.cvsignore
diff --git a/examples/VlibBeans/build.xml b/tapestry-examples/VlibBeans/build.xml
similarity index 100%
rename from examples/VlibBeans/build.xml
rename to tapestry-examples/VlibBeans/build.xml
diff --git a/examples/VlibBeans/createDb.sql b/tapestry-examples/VlibBeans/createDb.sql
similarity index 100%
rename from examples/VlibBeans/createDb.sql
rename to tapestry-examples/VlibBeans/createDb.sql
diff --git a/examples/VlibBeans/ejb-jar.xml b/tapestry-examples/VlibBeans/ejb-jar.xml
similarity index 100%
rename from examples/VlibBeans/ejb-jar.xml
rename to tapestry-examples/VlibBeans/ejb-jar.xml
diff --git a/examples/VlibBeans/jboss.xml b/tapestry-examples/VlibBeans/jboss.xml
similarity index 100%
rename from examples/VlibBeans/jboss.xml
rename to tapestry-examples/VlibBeans/jboss.xml
diff --git a/examples/VlibBeans/jbosscmp-jdbc.xml b/tapestry-examples/VlibBeans/jbosscmp-jdbc.xml
similarity index 100%
rename from examples/VlibBeans/jbosscmp-jdbc.xml
rename to tapestry-examples/VlibBeans/jbosscmp-jdbc.xml
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/Book.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/Book.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/Book.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/Book.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/BorrowException.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/BorrowException.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/BorrowException.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/BorrowException.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IBook.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IBook.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IBook.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IBook.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IBookHome.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IBookHome.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IBookHome.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IBookHome.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IBookQuery.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IBookQuery.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IBookQuery.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IBookQuery.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IBookQueryHome.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IBookQueryHome.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IBookQueryHome.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IBookQueryHome.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IEntityBean.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IEntityBean.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IEntityBean.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IEntityBean.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IKeyAllocator.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IKeyAllocator.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IKeyAllocator.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IKeyAllocator.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IKeyAllocatorHome.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IKeyAllocatorHome.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IKeyAllocatorHome.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IKeyAllocatorHome.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IOperations.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IOperations.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IOperations.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IOperations.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IOperationsHome.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IOperationsHome.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IOperationsHome.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IOperationsHome.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IPerson.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IPerson.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IPerson.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IPerson.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IPersonHome.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IPersonHome.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IPersonHome.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IPersonHome.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IPublisher.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IPublisher.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IPublisher.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IPublisher.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IPublisherHome.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IPublisherHome.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IPublisherHome.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/IPublisherHome.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/LoginException.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/LoginException.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/LoginException.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/LoginException.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/MasterQueryParameters.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/MasterQueryParameters.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/MasterQueryParameters.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/MasterQueryParameters.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/Person.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/Person.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/Person.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/Person.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/Publisher.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/Publisher.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/Publisher.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/Publisher.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/RegistrationException.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/RegistrationException.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/RegistrationException.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/RegistrationException.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/SortColumn.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/SortColumn.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/SortColumn.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/SortColumn.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/SortOrdering.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/SortOrdering.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/SortOrdering.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/SortOrdering.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/AbstractEntityBean.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/AbstractEntityBean.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/AbstractEntityBean.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/AbstractEntityBean.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/BookBean.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/BookBean.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/BookBean.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/BookBean.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/BookQueryBean.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/BookQueryBean.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/BookQueryBean.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/BookQueryBean.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/KeyAllocatorBean.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/KeyAllocatorBean.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/KeyAllocatorBean.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/KeyAllocatorBean.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/OperationsBean.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/OperationsBean.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/OperationsBean.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/OperationsBean.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/PersonBean.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/PersonBean.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/PersonBean.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/PersonBean.java
diff --git a/examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/PublisherBean.java b/tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/PublisherBean.java
similarity index 100%
rename from examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/PublisherBean.java
rename to tapestry-examples/VlibBeans/src/org/apache/tapestry/vlib/ejb/impl/PublisherBean.java
diff --git a/examples/VlibEAR/META-INF/application.xml b/tapestry-examples/VlibEAR/META-INF/application.xml
similarity index 100%
rename from examples/VlibEAR/META-INF/application.xml
rename to tapestry-examples/VlibEAR/META-INF/application.xml
diff --git a/examples/VlibEAR/build.xml b/tapestry-examples/VlibEAR/build.xml
similarity index 100%
rename from examples/VlibEAR/build.xml
rename to tapestry-examples/VlibEAR/build.xml
diff --git a/tapestry-examples/build.xml b/tapestry-examples/build.xml
new file mode 100644
index 0000000..f812cf2
--- /dev/null
+++ b/tapestry-examples/build.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+<!-- Interface between the top-level build file and any of the examples.  Simply re-executes 
+     its targets in each of its sub-projects. -->
+<project name="Tapestry Examples" default="install">
+	<target name="clean">
+		<ant dir="tapestry-workbench" target="clean"/>
+		<ant dir="VlibBeans" target="clean"/>
+		<ant dir="Vlib" target="clean"/>
+	</target>
+	<target name="install">
+		<ant dir="tapestry-workbench" target="install" inheritAll="false"/>
+		<ant dir="VlibBeans" target="install" inheritAll="false"/>
+		<ant dir="Vlib" target="install" inheritAll="false"/>
+		<ant dir="VlibEAR" target="install" inheritAll="false"/>
+	</target>
+</project>
diff --git a/tapestry-examples/pom.xml b/tapestry-examples/pom.xml
new file mode 100644
index 0000000..596ddcb
--- /dev/null
+++ b/tapestry-examples/pom.xml
@@ -0,0 +1,57 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.apache.tapestry</groupId>
+    <artifactId>tapestry-examples</artifactId>
+    <packaging>pom</packaging>
+    <version>3.0.5-SNAPSHOT</version>
+    <!-- This should change to tapestry-project -->
+    <parent>
+        <groupId>org.apache.tapestry</groupId>
+        <artifactId>tapestry-project</artifactId>
+        <version>3.0.5-SNAPSHOT</version>
+    </parent>
+    <name>Examples</name>
+    <description>Tapestry example applications</description>
+    <inceptionYear>2006</inceptionYear>
+
+    <modules>
+        <module>tapestry-workbench</module>
+    </modules>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.apache.tapestry</groupId>
+                <artifactId>tapestry-framework</artifactId>
+                <version>3.0.5-SNAPSHOT</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.tapestry</groupId>
+                <artifactId>tapestry-contrib</artifactId>
+                <version>3.0.5-SNAPSHOT</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <build>
+        <pluginManagement>
+            <plugins>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-compiler-plugin</artifactId>
+                    <version>2.0.2</version>
+                    <inherited>true</inherited>
+                    <configuration>
+                        <source>1.4</source>
+                        <target>1.4</target>
+                    </configuration>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+    </build>
+
+    <reporting>
+        <outputDirectory>../target/site/tapestry-examples</outputDirectory>
+    </reporting>
+</project>
diff --git a/examples/Workbench/.cvsignore b/tapestry-examples/tapestry-workbench/.cvsignore
similarity index 100%
rename from examples/Workbench/.cvsignore
rename to tapestry-examples/tapestry-workbench/.cvsignore
diff --git a/tapestry-examples/tapestry-workbench/build.xml b/tapestry-examples/tapestry-workbench/build.xml
new file mode 100644
index 0000000..00c86a1
--- /dev/null
+++ b/tapestry-examples/tapestry-workbench/build.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0"?>
+<!-- $Id$ -->
+<project name="Tapestry Workbench Example" default="install">
+	<property name="root.dir" value="../.."/>
+	<property file="${root.dir}/config/Version.properties"/>
+	<property file="${root.dir}/config/build.properties"/>
+	<property file="${root.dir}/config/common.properties"/>
+	
+	<property name="config.dir" value="config"/>
+	<property name="build.dir" value=".build"/>
+	
+	<path id="compile.classpath">
+		<fileset dir="${root.lib.dir}">
+			<include name="*.jar"/>
+			<include name="${ext.dir}/*.jar"/>
+			<include name="${j2ee.dir}/*.jar"/>
+		</fileset>
+		<fileset dir="${lib.dir}">
+			<include name="*.jar"/>	
+		</fileset>
+	</path>
+	<target name="init">
+		<mkdir dir="${classes.dir}"/>
+	</target>
+	<target name="clean">
+		<delete dir="${classes.dir}" quiet="true"/>
+		<delete dir="${build.dir}" quiet="true"/>
+	</target>
+	<target name="compile" depends="init"
+		description="Compile all classes in the tutorial.">
+		<javac srcdir="${src.dir}" destdir="${classes.dir}" debug="on"
+			target="1.1" source="1.3">
+			<classpath refid="compile.classpath"/>
+		</javac>
+	</target>
+	<target name="install" depends="compile"
+		description="Compile all classes and build the installed WAR.">
+		
+		<mkdir dir="${build.dir}"/>
+		<mkdir dir="${examples.dir}"/>
+		
+		<copy file="context/WEB-INF/web.xml" todir="${build.dir}">
+			<filterset>
+				<filter token="TAPESTRY_JAR" value="${framework.jar}"/>
+			</filterset>
+		</copy>
+		
+		<war warfile="${examples.dir}/${workbench.war}"
+			webxml="${build.dir}/web.xml">
+			
+			<fileset dir="context"/>
+						
+			<classes dir="${classes.dir}"/>
+			<classes dir="${src.dir}">
+				<exclude name="**/*.java"/>
+				<exclude name="**/package.html"/>
+			</classes>
+			<classes dir="${root.config.dir}">
+			  <include name="log4j.properties"/>
+			</classes>
+			<lib dir="${lib.dir}">
+				<include name="*.jar"/>
+			</lib>
+			<lib dir="${root.lib.dir}">
+				<include name="*.jar"/>
+			</lib>			
+			<lib dir="${root.lib.dir}/${ext.dir}">
+			  <include name="*.jar"/>
+			</lib>
+			<lib dir="${root.lib.dir}/${runtime.dir}">
+			  <include name="*.jar"/>
+			</lib>
+		</war>
+	</target>
+	
+</project>
diff --git a/examples/Workbench/context/Chart.html b/tapestry-examples/tapestry-workbench/context/Chart.html
similarity index 100%
rename from examples/Workbench/context/Chart.html
rename to tapestry-examples/tapestry-workbench/context/Chart.html
diff --git a/examples/Workbench/context/Dates.html b/tapestry-examples/tapestry-workbench/context/Dates.html
similarity index 100%
rename from examples/Workbench/context/Dates.html
rename to tapestry-examples/tapestry-workbench/context/Dates.html
diff --git a/examples/Workbench/context/ExceptionTab.html b/tapestry-examples/tapestry-workbench/context/ExceptionTab.html
similarity index 100%
rename from examples/Workbench/context/ExceptionTab.html
rename to tapestry-examples/tapestry-workbench/context/ExceptionTab.html
diff --git a/examples/Workbench/context/Fields.html b/tapestry-examples/tapestry-workbench/context/Fields.html
similarity index 100%
rename from examples/Workbench/context/Fields.html
rename to tapestry-examples/tapestry-workbench/context/Fields.html
diff --git a/examples/Workbench/context/FieldsResults.html b/tapestry-examples/tapestry-workbench/context/FieldsResults.html
similarity index 100%
rename from examples/Workbench/context/FieldsResults.html
rename to tapestry-examples/tapestry-workbench/context/FieldsResults.html
diff --git a/examples/Workbench/context/FileSystemTableTree.html b/tapestry-examples/tapestry-workbench/context/FileSystemTableTree.html
similarity index 100%
rename from examples/Workbench/context/FileSystemTableTree.html
rename to tapestry-examples/tapestry-workbench/context/FileSystemTableTree.html
diff --git a/examples/Workbench/context/FileSystemTree.html b/tapestry-examples/tapestry-workbench/context/FileSystemTree.html
similarity index 100%
rename from examples/Workbench/context/FileSystemTree.html
rename to tapestry-examples/tapestry-workbench/context/FileSystemTree.html
diff --git a/examples/Workbench/context/Home.html b/tapestry-examples/tapestry-workbench/context/Home.html
similarity index 100%
rename from examples/Workbench/context/Home.html
rename to tapestry-examples/tapestry-workbench/context/Home.html
diff --git a/examples/Workbench/context/JSP.html b/tapestry-examples/tapestry-workbench/context/JSP.html
similarity index 100%
rename from examples/Workbench/context/JSP.html
rename to tapestry-examples/tapestry-workbench/context/JSP.html
diff --git a/examples/Workbench/context/JSPResults.html b/tapestry-examples/tapestry-workbench/context/JSPResults.html
similarity index 100%
rename from examples/Workbench/context/JSPResults.html
rename to tapestry-examples/tapestry-workbench/context/JSPResults.html
diff --git a/examples/Workbench/context/Localization.html b/tapestry-examples/tapestry-workbench/context/Localization.html
similarity index 100%
rename from examples/Workbench/context/Localization.html
rename to tapestry-examples/tapestry-workbench/context/Localization.html
diff --git a/examples/Workbench/context/LocalizationChange.html b/tapestry-examples/tapestry-workbench/context/LocalizationChange.html
similarity index 100%
rename from examples/Workbench/context/LocalizationChange.html
rename to tapestry-examples/tapestry-workbench/context/LocalizationChange.html
diff --git a/examples/Workbench/context/LocalizationChange_de.html b/tapestry-examples/tapestry-workbench/context/LocalizationChange_de.html
similarity index 100%
rename from examples/Workbench/context/LocalizationChange_de.html
rename to tapestry-examples/tapestry-workbench/context/LocalizationChange_de.html
diff --git a/examples/Workbench/context/LocalizationChange_fr.html b/tapestry-examples/tapestry-workbench/context/LocalizationChange_fr.html
similarity index 100%
rename from examples/Workbench/context/LocalizationChange_fr.html
rename to tapestry-examples/tapestry-workbench/context/LocalizationChange_fr.html
diff --git a/examples/Workbench/context/LocalizationChange_it.html b/tapestry-examples/tapestry-workbench/context/LocalizationChange_it.html
similarity index 100%
rename from examples/Workbench/context/LocalizationChange_it.html
rename to tapestry-examples/tapestry-workbench/context/LocalizationChange_it.html
diff --git a/examples/Workbench/context/Localization_de.html b/tapestry-examples/tapestry-workbench/context/Localization_de.html
similarity index 100%
rename from examples/Workbench/context/Localization_de.html
rename to tapestry-examples/tapestry-workbench/context/Localization_de.html
diff --git a/examples/Workbench/context/Localization_fr.html b/tapestry-examples/tapestry-workbench/context/Localization_fr.html
similarity index 100%
rename from examples/Workbench/context/Localization_fr.html
rename to tapestry-examples/tapestry-workbench/context/Localization_fr.html
diff --git a/examples/Workbench/context/Localization_it.html b/tapestry-examples/tapestry-workbench/context/Localization_it.html
similarity index 100%
rename from examples/Workbench/context/Localization_it.html
rename to tapestry-examples/tapestry-workbench/context/Localization_it.html
diff --git a/examples/Workbench/context/Palette.html b/tapestry-examples/tapestry-workbench/context/Palette.html
similarity index 100%
rename from examples/Workbench/context/Palette.html
rename to tapestry-examples/tapestry-workbench/context/Palette.html
diff --git a/examples/Workbench/context/PaletteResults.html b/tapestry-examples/tapestry-workbench/context/PaletteResults.html
similarity index 100%
rename from examples/Workbench/context/PaletteResults.html
rename to tapestry-examples/tapestry-workbench/context/PaletteResults.html
diff --git a/examples/Workbench/context/Redirect.html b/tapestry-examples/tapestry-workbench/context/Redirect.html
similarity index 100%
rename from examples/Workbench/context/Redirect.html
rename to tapestry-examples/tapestry-workbench/context/Redirect.html
diff --git a/examples/Workbench/context/Table.html b/tapestry-examples/tapestry-workbench/context/Table.html
similarity index 100%
rename from examples/Workbench/context/Table.html
rename to tapestry-examples/tapestry-workbench/context/Table.html
diff --git a/examples/Workbench/context/TapestryTags.jsp b/tapestry-examples/tapestry-workbench/context/TapestryTags.jsp
similarity index 100%
rename from examples/Workbench/context/TapestryTags.jsp
rename to tapestry-examples/tapestry-workbench/context/TapestryTags.jsp
diff --git a/examples/Workbench/context/TreeHome.html b/tapestry-examples/tapestry-workbench/context/TreeHome.html
similarity index 100%
rename from examples/Workbench/context/TreeHome.html
rename to tapestry-examples/tapestry-workbench/context/TreeHome.html
diff --git a/examples/Workbench/context/Upload.html b/tapestry-examples/tapestry-workbench/context/Upload.html
similarity index 100%
rename from examples/Workbench/context/Upload.html
rename to tapestry-examples/tapestry-workbench/context/Upload.html
diff --git a/examples/Workbench/context/UploadResults.html b/tapestry-examples/tapestry-workbench/context/UploadResults.html
similarity index 100%
rename from examples/Workbench/context/UploadResults.html
rename to tapestry-examples/tapestry-workbench/context/UploadResults.html
diff --git a/examples/Workbench/context/WEB-INF/Border.html b/tapestry-examples/tapestry-workbench/context/WEB-INF/Border.html
similarity index 100%
rename from examples/Workbench/context/WEB-INF/Border.html
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/Border.html
diff --git a/examples/Workbench/context/WEB-INF/Border.jwc b/tapestry-examples/tapestry-workbench/context/WEB-INF/Border.jwc
similarity index 100%
rename from examples/Workbench/context/WEB-INF/Border.jwc
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/Border.jwc
diff --git a/examples/Workbench/context/WEB-INF/Border.properties b/tapestry-examples/tapestry-workbench/context/WEB-INF/Border.properties
similarity index 100%
rename from examples/Workbench/context/WEB-INF/Border.properties
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/Border.properties
diff --git a/examples/Workbench/context/WEB-INF/Border_de.properties b/tapestry-examples/tapestry-workbench/context/WEB-INF/Border_de.properties
similarity index 100%
rename from examples/Workbench/context/WEB-INF/Border_de.properties
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/Border_de.properties
diff --git a/examples/Workbench/context/WEB-INF/Border_fr.properties b/tapestry-examples/tapestry-workbench/context/WEB-INF/Border_fr.properties
similarity index 100%
rename from examples/Workbench/context/WEB-INF/Border_fr.properties
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/Border_fr.properties
diff --git a/examples/Workbench/context/WEB-INF/Border_it.properties b/tapestry-examples/tapestry-workbench/context/WEB-INF/Border_it.properties
similarity index 100%
rename from examples/Workbench/context/WEB-INF/Border_it.properties
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/Border_it.properties
diff --git a/examples/Workbench/context/WEB-INF/Chart.page b/tapestry-examples/tapestry-workbench/context/WEB-INF/Chart.page
similarity index 100%
rename from examples/Workbench/context/WEB-INF/Chart.page
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/Chart.page
diff --git a/examples/Workbench/context/WEB-INF/Dates.page b/tapestry-examples/tapestry-workbench/context/WEB-INF/Dates.page
similarity index 100%
rename from examples/Workbench/context/WEB-INF/Dates.page
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/Dates.page
diff --git a/examples/Workbench/context/WEB-INF/DirectoryTableView.html b/tapestry-examples/tapestry-workbench/context/WEB-INF/DirectoryTableView.html
similarity index 100%
rename from examples/Workbench/context/WEB-INF/DirectoryTableView.html
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/DirectoryTableView.html
diff --git a/examples/Workbench/context/WEB-INF/DirectoryTableView.jwc b/tapestry-examples/tapestry-workbench/context/WEB-INF/DirectoryTableView.jwc
similarity index 100%
rename from examples/Workbench/context/WEB-INF/DirectoryTableView.jwc
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/DirectoryTableView.jwc
diff --git a/examples/Workbench/context/WEB-INF/ErrorFest.page b/tapestry-examples/tapestry-workbench/context/WEB-INF/ErrorFest.page
similarity index 100%
rename from examples/Workbench/context/WEB-INF/ErrorFest.page
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/ErrorFest.page
diff --git a/examples/Workbench/context/WEB-INF/Fields.page b/tapestry-examples/tapestry-workbench/context/WEB-INF/Fields.page
similarity index 100%
rename from examples/Workbench/context/WEB-INF/Fields.page
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/Fields.page
diff --git a/examples/Workbench/context/WEB-INF/FieldsResults.page b/tapestry-examples/tapestry-workbench/context/WEB-INF/FieldsResults.page
similarity index 100%
rename from examples/Workbench/context/WEB-INF/FieldsResults.page
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/FieldsResults.page
diff --git a/examples/Workbench/context/WEB-INF/FileSystemTableTree.page b/tapestry-examples/tapestry-workbench/context/WEB-INF/FileSystemTableTree.page
similarity index 100%
rename from examples/Workbench/context/WEB-INF/FileSystemTableTree.page
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/FileSystemTableTree.page
diff --git a/examples/Workbench/context/WEB-INF/FileSystemTree.page b/tapestry-examples/tapestry-workbench/context/WEB-INF/FileSystemTree.page
similarity index 100%
rename from examples/Workbench/context/WEB-INF/FileSystemTree.page
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/FileSystemTree.page
diff --git a/examples/Workbench/context/WEB-INF/JSP.page b/tapestry-examples/tapestry-workbench/context/WEB-INF/JSP.page
similarity index 100%
rename from examples/Workbench/context/WEB-INF/JSP.page
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/JSP.page
diff --git a/examples/Workbench/context/WEB-INF/JSPResults.page b/tapestry-examples/tapestry-workbench/context/WEB-INF/JSPResults.page
similarity index 100%
rename from examples/Workbench/context/WEB-INF/JSPResults.page
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/JSPResults.page
diff --git a/examples/Workbench/context/WEB-INF/LocaleList.html b/tapestry-examples/tapestry-workbench/context/WEB-INF/LocaleList.html
similarity index 100%
rename from examples/Workbench/context/WEB-INF/LocaleList.html
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/LocaleList.html
diff --git a/examples/Workbench/context/WEB-INF/LocaleList.jwc b/tapestry-examples/tapestry-workbench/context/WEB-INF/LocaleList.jwc
similarity index 100%
rename from examples/Workbench/context/WEB-INF/LocaleList.jwc
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/LocaleList.jwc
diff --git a/examples/Workbench/context/WEB-INF/LocaleSelection.html b/tapestry-examples/tapestry-workbench/context/WEB-INF/LocaleSelection.html
similarity index 100%
rename from examples/Workbench/context/WEB-INF/LocaleSelection.html
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/LocaleSelection.html
diff --git a/examples/Workbench/context/WEB-INF/LocaleSelection.jwc b/tapestry-examples/tapestry-workbench/context/WEB-INF/LocaleSelection.jwc
similarity index 100%
rename from examples/Workbench/context/WEB-INF/LocaleSelection.jwc
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/LocaleSelection.jwc
diff --git a/examples/Workbench/context/WEB-INF/LocaleSelection.properties b/tapestry-examples/tapestry-workbench/context/WEB-INF/LocaleSelection.properties
similarity index 100%
rename from examples/Workbench/context/WEB-INF/LocaleSelection.properties
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/LocaleSelection.properties
diff --git a/examples/Workbench/context/WEB-INF/Localization.page b/tapestry-examples/tapestry-workbench/context/WEB-INF/Localization.page
similarity index 100%
rename from examples/Workbench/context/WEB-INF/Localization.page
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/Localization.page
diff --git a/examples/Workbench/context/WEB-INF/LocalizationChange.page b/tapestry-examples/tapestry-workbench/context/WEB-INF/LocalizationChange.page
similarity index 100%
rename from examples/Workbench/context/WEB-INF/LocalizationChange.page
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/LocalizationChange.page
diff --git a/examples/Workbench/context/WEB-INF/Palette.page b/tapestry-examples/tapestry-workbench/context/WEB-INF/Palette.page
similarity index 100%
rename from examples/Workbench/context/WEB-INF/Palette.page
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/Palette.page
diff --git a/examples/Workbench/context/WEB-INF/PaletteResults.page b/tapestry-examples/tapestry-workbench/context/WEB-INF/PaletteResults.page
similarity index 100%
rename from examples/Workbench/context/WEB-INF/PaletteResults.page
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/PaletteResults.page
diff --git a/examples/Workbench/context/WEB-INF/Redirect.page b/tapestry-examples/tapestry-workbench/context/WEB-INF/Redirect.page
similarity index 100%
rename from examples/Workbench/context/WEB-INF/Redirect.page
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/Redirect.page
diff --git a/examples/Workbench/context/WEB-INF/ShowError.html b/tapestry-examples/tapestry-workbench/context/WEB-INF/ShowError.html
similarity index 100%
rename from examples/Workbench/context/WEB-INF/ShowError.html
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/ShowError.html
diff --git a/examples/Workbench/context/WEB-INF/ShowError.jwc b/tapestry-examples/tapestry-workbench/context/WEB-INF/ShowError.jwc
similarity index 100%
rename from examples/Workbench/context/WEB-INF/ShowError.jwc
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/ShowError.jwc
diff --git a/examples/Workbench/context/WEB-INF/Table.page b/tapestry-examples/tapestry-workbench/context/WEB-INF/Table.page
similarity index 100%
rename from examples/Workbench/context/WEB-INF/Table.page
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/Table.page
diff --git a/examples/Workbench/context/WEB-INF/Upload.page b/tapestry-examples/tapestry-workbench/context/WEB-INF/Upload.page
similarity index 100%
rename from examples/Workbench/context/WEB-INF/Upload.page
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/Upload.page
diff --git a/examples/Workbench/context/WEB-INF/UploadResults.page b/tapestry-examples/tapestry-workbench/context/WEB-INF/UploadResults.page
similarity index 100%
rename from examples/Workbench/context/WEB-INF/UploadResults.page
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/UploadResults.page
diff --git a/tapestry-examples/tapestry-workbench/context/WEB-INF/web.xml b/tapestry-examples/tapestry-workbench/context/WEB-INF/web.xml
new file mode 100644
index 0000000..3fa3b04
--- /dev/null
+++ b/tapestry-examples/tapestry-workbench/context/WEB-INF/web.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+<!--$Id$ -->
+<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
+ "http://java.sun.com/dtd/web-app_2_3.dtd">
+<web-app>
+  <display-name>Tapestry Workbench Example</display-name>
+  
+	<filter>
+		<filter-name>redirect</filter-name>
+		<filter-class>org.apache.tapestry.RedirectFilter</filter-class>
+	</filter>
+	
+	<filter-mapping>
+		<filter-name>redirect</filter-name>
+		<url-pattern>/</url-pattern>
+	</filter-mapping>
+  
+  <servlet>
+  	<servlet-name>workbench</servlet-name>
+    <servlet-class>org.apache.tapestry.ApplicationServlet</servlet-class>
+  	<load-on-startup>0</load-on-startup>
+  </servlet>
+     
+  <servlet-mapping>
+  	<servlet-name>workbench</servlet-name>
+  	<url-pattern>/app</url-pattern>
+  </servlet-mapping>  
+
+  <session-config>
+  	<session-timeout>15</session-timeout>
+  </session-config>
+
+  <taglib>
+  	<taglib-uri>http://jakarta.apache.org/tapestry/tld/tapestry_1_0.tld</taglib-uri>
+  	<taglib-location>/WEB-INF/lib/tapestry-framework-3.0.5-SNAPSHOT.jar</taglib-location>
+  </taglib>
+</web-app>
diff --git a/examples/Workbench/context/WEB-INF/workbench.application b/tapestry-examples/tapestry-workbench/context/WEB-INF/workbench.application
similarity index 100%
rename from examples/Workbench/context/WEB-INF/workbench.application
rename to tapestry-examples/tapestry-workbench/context/WEB-INF/workbench.application
diff --git a/examples/Workbench/context/css/workbench.css b/tapestry-examples/tapestry-workbench/context/css/workbench.css
similarity index 100%
rename from examples/Workbench/context/css/workbench.css
rename to tapestry-examples/tapestry-workbench/context/css/workbench.css
diff --git a/examples/Workbench/context/images/.cvsignore b/tapestry-examples/tapestry-workbench/context/images/.cvsignore
similarity index 100%
rename from examples/Workbench/context/images/.cvsignore
rename to tapestry-examples/tapestry-workbench/context/images/.cvsignore
diff --git a/examples/Workbench/context/images/Back-focus.gif b/tapestry-examples/tapestry-workbench/context/images/Back-focus.gif
similarity index 100%
rename from examples/Workbench/context/images/Back-focus.gif
rename to tapestry-examples/tapestry-workbench/context/images/Back-focus.gif
Binary files differ
diff --git a/examples/Workbench/context/images/Back-focus_de.gif b/tapestry-examples/tapestry-workbench/context/images/Back-focus_de.gif
similarity index 100%
rename from examples/Workbench/context/images/Back-focus_de.gif
rename to tapestry-examples/tapestry-workbench/context/images/Back-focus_de.gif
Binary files differ
diff --git a/examples/Workbench/context/images/Back-focus_fr.gif b/tapestry-examples/tapestry-workbench/context/images/Back-focus_fr.gif
similarity index 100%
rename from examples/Workbench/context/images/Back-focus_fr.gif
rename to tapestry-examples/tapestry-workbench/context/images/Back-focus_fr.gif
Binary files differ
diff --git a/examples/Workbench/context/images/Back-focus_it.gif b/tapestry-examples/tapestry-workbench/context/images/Back-focus_it.gif
similarity index 100%
rename from examples/Workbench/context/images/Back-focus_it.gif
rename to tapestry-examples/tapestry-workbench/context/images/Back-focus_it.gif
Binary files differ
diff --git a/examples/Workbench/context/images/Back.gif b/tapestry-examples/tapestry-workbench/context/images/Back.gif
similarity index 100%
rename from examples/Workbench/context/images/Back.gif
rename to tapestry-examples/tapestry-workbench/context/images/Back.gif
Binary files differ
diff --git a/examples/Workbench/context/images/Back_de.gif b/tapestry-examples/tapestry-workbench/context/images/Back_de.gif
similarity index 100%
rename from examples/Workbench/context/images/Back_de.gif
rename to tapestry-examples/tapestry-workbench/context/images/Back_de.gif
Binary files differ
diff --git a/examples/Workbench/context/images/Back_fr.gif b/tapestry-examples/tapestry-workbench/context/images/Back_fr.gif
similarity index 100%
rename from examples/Workbench/context/images/Back_fr.gif
rename to tapestry-examples/tapestry-workbench/context/images/Back_fr.gif
Binary files differ
diff --git a/examples/Workbench/context/images/Back_it.gif b/tapestry-examples/tapestry-workbench/context/images/Back_it.gif
similarity index 100%
rename from examples/Workbench/context/images/Back_it.gif
rename to tapestry-examples/tapestry-workbench/context/images/Back_it.gif
Binary files differ
diff --git a/examples/Workbench/context/images/Change.gif b/tapestry-examples/tapestry-workbench/context/images/Change.gif
similarity index 100%
rename from examples/Workbench/context/images/Change.gif
rename to tapestry-examples/tapestry-workbench/context/images/Change.gif
Binary files differ
diff --git a/examples/Workbench/context/images/Change_de.gif b/tapestry-examples/tapestry-workbench/context/images/Change_de.gif
similarity index 100%
rename from examples/Workbench/context/images/Change_de.gif
rename to tapestry-examples/tapestry-workbench/context/images/Change_de.gif
Binary files differ
diff --git a/examples/Workbench/context/images/Change_fr.gif b/tapestry-examples/tapestry-workbench/context/images/Change_fr.gif
similarity index 100%
rename from examples/Workbench/context/images/Change_fr.gif
rename to tapestry-examples/tapestry-workbench/context/images/Change_fr.gif
Binary files differ
diff --git a/examples/Workbench/context/images/Change_it.gif b/tapestry-examples/tapestry-workbench/context/images/Change_it.gif
similarity index 100%
rename from examples/Workbench/context/images/Change_it.gif
rename to tapestry-examples/tapestry-workbench/context/images/Change_it.gif
Binary files differ
diff --git a/examples/Workbench/context/images/Continue.gif b/tapestry-examples/tapestry-workbench/context/images/Continue.gif
similarity index 100%
rename from examples/Workbench/context/images/Continue.gif
rename to tapestry-examples/tapestry-workbench/context/images/Continue.gif
Binary files differ
diff --git a/examples/Workbench/context/images/Update.gif b/tapestry-examples/tapestry-workbench/context/images/Update.gif
similarity index 100%
rename from examples/Workbench/context/images/Update.gif
rename to tapestry-examples/tapestry-workbench/context/images/Update.gif
Binary files differ
diff --git a/examples/Workbench/context/images/Warning-small.gif b/tapestry-examples/tapestry-workbench/context/images/Warning-small.gif
similarity index 100%
rename from examples/Workbench/context/images/Warning-small.gif
rename to tapestry-examples/tapestry-workbench/context/images/Warning-small.gif
Binary files differ
diff --git a/examples/Workbench/context/images/Warning.gif b/tapestry-examples/tapestry-workbench/context/images/Warning.gif
similarity index 100%
rename from examples/Workbench/context/images/Warning.gif
rename to tapestry-examples/tapestry-workbench/context/images/Warning.gif
Binary files differ
diff --git a/examples/Workbench/context/images/minus.gif b/tapestry-examples/tapestry-workbench/context/images/minus.gif
similarity index 100%
rename from examples/Workbench/context/images/minus.gif
rename to tapestry-examples/tapestry-workbench/context/images/minus.gif
Binary files differ
diff --git a/examples/Workbench/context/images/nodeimage.gif b/tapestry-examples/tapestry-workbench/context/images/nodeimage.gif
similarity index 100%
rename from examples/Workbench/context/images/nodeimage.gif
rename to tapestry-examples/tapestry-workbench/context/images/nodeimage.gif
Binary files differ
diff --git a/examples/Workbench/context/images/plus.gif b/tapestry-examples/tapestry-workbench/context/images/plus.gif
similarity index 100%
rename from examples/Workbench/context/images/plus.gif
rename to tapestry-examples/tapestry-workbench/context/images/plus.gif
Binary files differ
diff --git a/examples/Workbench/context/images/tab-active-left.gif b/tapestry-examples/tapestry-workbench/context/images/tab-active-left.gif
similarity index 100%
rename from examples/Workbench/context/images/tab-active-left.gif
rename to tapestry-examples/tapestry-workbench/context/images/tab-active-left.gif
Binary files differ
diff --git a/examples/Workbench/context/images/tab-active-mid.gif b/tapestry-examples/tapestry-workbench/context/images/tab-active-mid.gif
similarity index 100%
rename from examples/Workbench/context/images/tab-active-mid.gif
rename to tapestry-examples/tapestry-workbench/context/images/tab-active-mid.gif
Binary files differ
diff --git a/examples/Workbench/context/images/tab-active-right.gif b/tapestry-examples/tapestry-workbench/context/images/tab-active-right.gif
similarity index 100%
rename from examples/Workbench/context/images/tab-active-right.gif
rename to tapestry-examples/tapestry-workbench/context/images/tab-active-right.gif
Binary files differ
diff --git a/examples/Workbench/context/images/tab-inactive-left.gif b/tapestry-examples/tapestry-workbench/context/images/tab-inactive-left.gif
similarity index 100%
rename from examples/Workbench/context/images/tab-inactive-left.gif
rename to tapestry-examples/tapestry-workbench/context/images/tab-inactive-left.gif
Binary files differ
diff --git a/examples/Workbench/context/images/tab-inactive-mid.gif b/tapestry-examples/tapestry-workbench/context/images/tab-inactive-mid.gif
similarity index 100%
rename from examples/Workbench/context/images/tab-inactive-mid.gif
rename to tapestry-examples/tapestry-workbench/context/images/tab-inactive-mid.gif
Binary files differ
diff --git a/examples/Workbench/context/images/tab-inactive-right.gif b/tapestry-examples/tapestry-workbench/context/images/tab-inactive-right.gif
similarity index 100%
rename from examples/Workbench/context/images/tab-inactive-right.gif
rename to tapestry-examples/tapestry-workbench/context/images/tab-inactive-right.gif
Binary files differ
diff --git a/examples/Workbench/context/popuplink-help.html b/tapestry-examples/tapestry-workbench/context/popuplink-help.html
similarity index 100%
rename from examples/Workbench/context/popuplink-help.html
rename to tapestry-examples/tapestry-workbench/context/popuplink-help.html
diff --git a/examples/Workbench/context/redirect-target.html b/tapestry-examples/tapestry-workbench/context/redirect-target.html
similarity index 100%
rename from examples/Workbench/context/redirect-target.html
rename to tapestry-examples/tapestry-workbench/context/redirect-target.html
diff --git a/examples/Workbench/image-src/.cvsignore b/tapestry-examples/tapestry-workbench/image-src/.cvsignore
similarity index 100%
rename from examples/Workbench/image-src/.cvsignore
rename to tapestry-examples/tapestry-workbench/image-src/.cvsignore
diff --git a/examples/Workbench/image-src/ActiveBlank.psp b/tapestry-examples/tapestry-workbench/image-src/ActiveBlank.psp
similarity index 100%
rename from examples/Workbench/image-src/ActiveBlank.psp
rename to tapestry-examples/tapestry-workbench/image-src/ActiveBlank.psp
Binary files differ
diff --git a/examples/Workbench/image-src/Back.psp b/tapestry-examples/tapestry-workbench/image-src/Back.psp
similarity index 100%
rename from examples/Workbench/image-src/Back.psp
rename to tapestry-examples/tapestry-workbench/image-src/Back.psp
Binary files differ
diff --git a/examples/Workbench/image-src/BlankFlat.psp b/tapestry-examples/tapestry-workbench/image-src/BlankFlat.psp
similarity index 100%
rename from examples/Workbench/image-src/BlankFlat.psp
rename to tapestry-examples/tapestry-workbench/image-src/BlankFlat.psp
Binary files differ
diff --git a/examples/Workbench/image-src/Change.psp b/tapestry-examples/tapestry-workbench/image-src/Change.psp
similarity index 100%
rename from examples/Workbench/image-src/Change.psp
rename to tapestry-examples/tapestry-workbench/image-src/Change.psp
Binary files differ
diff --git a/examples/Workbench/image-src/InactiveBlank.psp b/tapestry-examples/tapestry-workbench/image-src/InactiveBlank.psp
similarity index 100%
rename from examples/Workbench/image-src/InactiveBlank.psp
rename to tapestry-examples/tapestry-workbench/image-src/InactiveBlank.psp
Binary files differ
diff --git a/examples/Workbench/jetty.xml b/tapestry-examples/tapestry-workbench/jetty.xml
similarity index 100%
rename from examples/Workbench/jetty.xml
rename to tapestry-examples/tapestry-workbench/jetty.xml
diff --git a/examples/Workbench/lib/LICENSE.jCharts.txt b/tapestry-examples/tapestry-workbench/lib/LICENSE.jCharts.txt
similarity index 100%
rename from examples/Workbench/lib/LICENSE.jCharts.txt
rename to tapestry-examples/tapestry-workbench/lib/LICENSE.jCharts.txt
diff --git a/examples/Workbench/lib/jCharts-0.6.0.jar b/tapestry-examples/tapestry-workbench/lib/jCharts-0.6.0.jar
similarity index 100%
rename from examples/Workbench/lib/jCharts-0.6.0.jar
rename to tapestry-examples/tapestry-workbench/lib/jCharts-0.6.0.jar
Binary files differ
diff --git a/tapestry-examples/tapestry-workbench/pom.xml b/tapestry-examples/tapestry-workbench/pom.xml
new file mode 100644
index 0000000..ccee15f
--- /dev/null
+++ b/tapestry-examples/tapestry-workbench/pom.xml
@@ -0,0 +1,110 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.apache.tapestry</groupId>
+    <artifactId>tapestry-Workbench</artifactId>
+    <packaging>war</packaging>
+    <version>3.0.5-SNAPSHOT</version>
+
+    <!-- This should change to tapestry-project -->
+    <parent>
+        <groupId>org.apache.tapestry</groupId>
+        <artifactId>tapestry-examples</artifactId>
+        <version>3.0.5-SNAPSHOT</version>
+    </parent>
+
+    <name>Tapestry Workbench</name>
+    <inceptionYear>2006</inceptionYear>
+
+    <dependencies>
+        <dependency>
+            <groupId>jcharts</groupId>
+            <artifactId>jcharts</artifactId>
+            <version>0.6.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.tapestry</groupId>
+            <artifactId>tapestry-contrib</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.tapestry</groupId>
+            <artifactId>tapestry-framework</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>log4j</groupId>
+            <artifactId>log4j</artifactId>
+            <version>1.2.13</version>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>servlet-api</artifactId>
+            <version>2.3</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <sourceDirectory>src</sourceDirectory>
+
+        <resources>
+            <resource>
+                <directory>src</directory>
+                <includes>
+                    <include>**/*.gif</include>
+                    <include>**/*.png</include>
+                    <include>**/*.jwc</include>
+                    <include>**/*.page</include>
+                    <include>**/*.html</include>
+                    <include>**/*.properties</include>
+                </includes>
+            </resource>
+        </resources>
+
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>2.0.2</version>
+                <configuration>
+                    <source>1.4</source>
+                    <target>1.4</target>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.mortbay.jetty</groupId>
+                <artifactId>maven-jetty-plugin</artifactId>
+                <version>6.1.3</version>
+                <configuration>
+                    <webAppSourceDirectory>${basedir}/context</webAppSourceDirectory>
+                    <contextPath>/</contextPath>
+                    <systemProperties>
+                        <systemProperty>
+                            <name>org.apache.tapestry.disable-caching</name>
+                            <value>true</value>
+                        </systemProperty>
+                    </systemProperties>
+                </configuration>
+                <dependencies>
+                    <dependency>
+                        <groupId>commons-logging</groupId>
+                        <artifactId>commons-logging</artifactId>
+                        <version>1.0.4</version>
+                    </dependency>
+                    <dependency>
+                        <groupId>log4j</groupId>
+                        <artifactId>log4j</artifactId>
+                        <version>1.2.13</version>
+                    </dependency>
+                </dependencies>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-war-plugin</artifactId>
+                <version>2.0.2</version>
+                <configuration>
+                    <warSourceDirectory>${basedir}/context</warSourceDirectory>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/Redirect.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/Redirect.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/Redirect.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/Redirect.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/RequestDecoder.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/RequestDecoder.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/RequestDecoder.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/RequestDecoder.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/Visit.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/Visit.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/Visit.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/Visit.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/WorkbenchHomeService.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/WorkbenchHomeService.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/WorkbenchHomeService.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/WorkbenchHomeService.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/WorkbenchValidationDelegate.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/WorkbenchValidationDelegate.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/WorkbenchValidationDelegate.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/WorkbenchValidationDelegate.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/chart/ChartAsset.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/chart/ChartAsset.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/chart/ChartAsset.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/chart/ChartAsset.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/chart/ChartPage.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/chart/ChartPage.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/chart/ChartPage.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/chart/ChartPage.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/chart/ChartService.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/chart/ChartService.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/chart/ChartService.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/chart/ChartService.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/chart/IChartProvider.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/chart/IChartProvider.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/chart/IChartProvider.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/chart/IChartProvider.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/chart/PlotValue.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/chart/PlotValue.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/chart/PlotValue.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/chart/PlotValue.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/components/.cvsignore b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/components/.cvsignore
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/components/.cvsignore
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/components/.cvsignore
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/components/Border.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/components/Border.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/components/Border.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/components/Border.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/fields/Dates.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/fields/Dates.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/fields/Dates.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/fields/Dates.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/fields/Fields.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/fields/Fields.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/fields/Fields.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/fields/Fields.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/jsp/JSP.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/jsp/JSP.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/jsp/JSP.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/jsp/JSP.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/jsp/JSPResults.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/jsp/JSPResults.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/jsp/JSPResults.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/jsp/JSPResults.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/localization/LocaleModel.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/localization/LocaleModel.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/localization/LocaleModel.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/localization/LocaleModel.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/localization/Localization.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/localization/Localization.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/localization/Localization.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/localization/Localization.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/localization/LocalizationChange.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/localization/LocalizationChange.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/localization/LocalizationChange.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/localization/LocalizationChange.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/palette/Palette.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/palette/Palette.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/palette/Palette.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/palette/Palette.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/palette/PaletteResults.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/palette/PaletteResults.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/palette/PaletteResults.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/palette/PaletteResults.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/palette/SortModeStrings.properties b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/palette/SortModeStrings.properties
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/palette/SortModeStrings.properties
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/palette/SortModeStrings.properties
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/table/ILocaleSelectionListener.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/table/ILocaleSelectionListener.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/table/ILocaleSelectionListener.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/table/ILocaleSelectionListener.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/table/LocaleList.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/table/LocaleList.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/table/LocaleList.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/table/LocaleList.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/table/LocaleSelection.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/table/LocaleSelection.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/table/LocaleSelection.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/table/LocaleSelection.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/table/VerbosityRating.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/table/VerbosityRating.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/table/VerbosityRating.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/table/VerbosityRating.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/DirectoryTableView.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/DirectoryTableView.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/DirectoryTableView.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/DirectoryTableView.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/FileSystemTree.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/FileSystemTree.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/FileSystemTree.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/FileSystemTree.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/FileSystemTreeTable.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/FileSystemTreeTable.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/FileSystemTreeTable.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/FileSystemTreeTable.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/ISelectedFolderSource.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/ISelectedFolderSource.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/ISelectedFolderSource.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/ISelectedFolderSource.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/SessionVisit.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/SessionVisit.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/SessionVisit.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/SessionVisit.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/SimpleTree.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/SimpleTree.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/SimpleTree.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/SimpleTree.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/TestTreeNode.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/TestTreeNode.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/TestTreeNode.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/TestTreeNode.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/AssetsHolder.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/AssetsHolder.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/AssetsHolder.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/AssetsHolder.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/Drive.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/Drive.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/Drive.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/Drive.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/FileObject.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/FileObject.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/FileObject.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/FileObject.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/FileSystem.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/FileSystem.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/FileSystem.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/FileSystem.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/FileSystemDataModel.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/FileSystemDataModel.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/FileSystemDataModel.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/FileSystemDataModel.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/FileSystemStateManager.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/FileSystemStateManager.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/FileSystemStateManager.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/FileSystemStateManager.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/FolderObject.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/FolderObject.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/FolderObject.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/FolderObject.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/IFileSystemTreeNode.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/IFileSystemTreeNode.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/IFileSystemTreeNode.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/IFileSystemTreeNode.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/NodeRenderFactory.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/NodeRenderFactory.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/NodeRenderFactory.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/NodeRenderFactory.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/SFObject.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/SFObject.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/SFObject.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/SFObject.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/TreeClosed.gif b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/TreeClosed.gif
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/TreeClosed.gif
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/TreeClosed.gif
Binary files differ
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/TreeOpen.gif b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/TreeOpen.gif
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/TreeOpen.gif
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/TreeOpen.gif
Binary files differ
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/computer.gif b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/computer.gif
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/computer.gif
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/computer.gif
Binary files differ
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/file.gif b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/file.gif
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/file.gif
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/file.gif
Binary files differ
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/harddrive.gif b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/harddrive.gif
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/harddrive.gif
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/tree/examples/fsmodel/harddrive.gif
Binary files differ
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/upload/Upload.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/upload/Upload.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/upload/Upload.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/upload/Upload.java
diff --git a/examples/Workbench/src/org/apache/tapestry/workbench/upload/UploadResults.java b/tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/upload/UploadResults.java
similarity index 100%
rename from examples/Workbench/src/org/apache/tapestry/workbench/upload/UploadResults.java
rename to tapestry-examples/tapestry-workbench/src/org/apache/tapestry/workbench/upload/UploadResults.java
diff --git a/examples/wap/.cvsignore b/tapestry-examples/wap/.cvsignore
similarity index 100%
rename from examples/wap/.cvsignore
rename to tapestry-examples/wap/.cvsignore
diff --git a/examples/wap/build.xml b/tapestry-examples/wap/build.xml
similarity index 100%
rename from examples/wap/build.xml
rename to tapestry-examples/wap/build.xml
diff --git a/examples/wap/context/WEB-INF/animate/Home.page b/tapestry-examples/wap/context/WEB-INF/animate/Home.page
similarity index 100%
rename from examples/wap/context/WEB-INF/animate/Home.page
rename to tapestry-examples/wap/context/WEB-INF/animate/Home.page
diff --git a/examples/wap/context/WEB-INF/animate/animate.application b/tapestry-examples/wap/context/WEB-INF/animate/animate.application
similarity index 100%
rename from examples/wap/context/WEB-INF/animate/animate.application
rename to tapestry-examples/wap/context/WEB-INF/animate/animate.application
diff --git a/examples/wap/context/WEB-INF/hello/Hello.page b/tapestry-examples/wap/context/WEB-INF/hello/Hello.page
similarity index 100%
rename from examples/wap/context/WEB-INF/hello/Hello.page
rename to tapestry-examples/wap/context/WEB-INF/hello/Hello.page
diff --git a/examples/wap/context/WEB-INF/hello/Home.page b/tapestry-examples/wap/context/WEB-INF/hello/Home.page
similarity index 100%
rename from examples/wap/context/WEB-INF/hello/Home.page
rename to tapestry-examples/wap/context/WEB-INF/hello/Home.page
diff --git a/examples/wap/context/WEB-INF/hello/hello.application b/tapestry-examples/wap/context/WEB-INF/hello/hello.application
similarity index 100%
rename from examples/wap/context/WEB-INF/hello/hello.application
rename to tapestry-examples/wap/context/WEB-INF/hello/hello.application
diff --git a/examples/wap/context/WEB-INF/quiz/Home.page b/tapestry-examples/wap/context/WEB-INF/quiz/Home.page
similarity index 100%
rename from examples/wap/context/WEB-INF/quiz/Home.page
rename to tapestry-examples/wap/context/WEB-INF/quiz/Home.page
diff --git a/examples/wap/context/WEB-INF/quiz/Quiz.page b/tapestry-examples/wap/context/WEB-INF/quiz/Quiz.page
similarity index 100%
rename from examples/wap/context/WEB-INF/quiz/Quiz.page
rename to tapestry-examples/wap/context/WEB-INF/quiz/Quiz.page
diff --git a/examples/wap/context/WEB-INF/quiz/Scores.page b/tapestry-examples/wap/context/WEB-INF/quiz/Scores.page
similarity index 100%
rename from examples/wap/context/WEB-INF/quiz/Scores.page
rename to tapestry-examples/wap/context/WEB-INF/quiz/Scores.page
diff --git a/examples/wap/context/WEB-INF/quiz/easyquestions.txt b/tapestry-examples/wap/context/WEB-INF/quiz/easyquestions.txt
similarity index 100%
rename from examples/wap/context/WEB-INF/quiz/easyquestions.txt
rename to tapestry-examples/wap/context/WEB-INF/quiz/easyquestions.txt
diff --git a/examples/wap/context/WEB-INF/quiz/hardquestions.txt b/tapestry-examples/wap/context/WEB-INF/quiz/hardquestions.txt
similarity index 100%
rename from examples/wap/context/WEB-INF/quiz/hardquestions.txt
rename to tapestry-examples/wap/context/WEB-INF/quiz/hardquestions.txt
diff --git a/examples/wap/context/WEB-INF/quiz/mediumquestions.txt b/tapestry-examples/wap/context/WEB-INF/quiz/mediumquestions.txt
similarity index 100%
rename from examples/wap/context/WEB-INF/quiz/mediumquestions.txt
rename to tapestry-examples/wap/context/WEB-INF/quiz/mediumquestions.txt
diff --git a/examples/wap/context/WEB-INF/quiz/quiz.application b/tapestry-examples/wap/context/WEB-INF/quiz/quiz.application
similarity index 100%
rename from examples/wap/context/WEB-INF/quiz/quiz.application
rename to tapestry-examples/wap/context/WEB-INF/quiz/quiz.application
diff --git a/examples/wap/context/WEB-INF/web.xml b/tapestry-examples/wap/context/WEB-INF/web.xml
similarity index 100%
rename from examples/wap/context/WEB-INF/web.xml
rename to tapestry-examples/wap/context/WEB-INF/web.xml
diff --git a/examples/wap/context/animate/Home.wml b/tapestry-examples/wap/context/animate/Home.wml
similarity index 100%
rename from examples/wap/context/animate/Home.wml
rename to tapestry-examples/wap/context/animate/Home.wml
diff --git a/examples/wap/context/animate/images/img1.wbmp b/tapestry-examples/wap/context/animate/images/img1.wbmp
similarity index 100%
rename from examples/wap/context/animate/images/img1.wbmp
rename to tapestry-examples/wap/context/animate/images/img1.wbmp
Binary files differ
diff --git a/examples/wap/context/animate/images/img2.wbmp b/tapestry-examples/wap/context/animate/images/img2.wbmp
similarity index 100%
rename from examples/wap/context/animate/images/img2.wbmp
rename to tapestry-examples/wap/context/animate/images/img2.wbmp
Binary files differ
diff --git a/examples/wap/context/animate/images/img3.wbmp b/tapestry-examples/wap/context/animate/images/img3.wbmp
similarity index 100%
rename from examples/wap/context/animate/images/img3.wbmp
rename to tapestry-examples/wap/context/animate/images/img3.wbmp
Binary files differ
diff --git a/examples/wap/context/animate/images/img4.wbmp b/tapestry-examples/wap/context/animate/images/img4.wbmp
similarity index 100%
rename from examples/wap/context/animate/images/img4.wbmp
rename to tapestry-examples/wap/context/animate/images/img4.wbmp
Binary files differ
diff --git a/examples/wap/context/hello/Hello.wml b/tapestry-examples/wap/context/hello/Hello.wml
similarity index 100%
rename from examples/wap/context/hello/Hello.wml
rename to tapestry-examples/wap/context/hello/Hello.wml
diff --git a/examples/wap/context/hello/Home.wml b/tapestry-examples/wap/context/hello/Home.wml
similarity index 100%
rename from examples/wap/context/hello/Home.wml
rename to tapestry-examples/wap/context/hello/Home.wml
diff --git a/examples/wap/context/index.wml b/tapestry-examples/wap/context/index.wml
similarity index 100%
rename from examples/wap/context/index.wml
rename to tapestry-examples/wap/context/index.wml
diff --git a/examples/wap/context/quiz/Home.wml b/tapestry-examples/wap/context/quiz/Home.wml
similarity index 100%
rename from examples/wap/context/quiz/Home.wml
rename to tapestry-examples/wap/context/quiz/Home.wml
diff --git a/examples/wap/context/quiz/Quiz.wml b/tapestry-examples/wap/context/quiz/Quiz.wml
similarity index 100%
rename from examples/wap/context/quiz/Quiz.wml
rename to tapestry-examples/wap/context/quiz/Quiz.wml
diff --git a/examples/wap/context/quiz/Scores.wml b/tapestry-examples/wap/context/quiz/Scores.wml
similarity index 100%
rename from examples/wap/context/quiz/Scores.wml
rename to tapestry-examples/wap/context/quiz/Scores.wml
diff --git a/examples/wap/context/quiz/images/logo.wbmp b/tapestry-examples/wap/context/quiz/images/logo.wbmp
similarity index 100%
rename from examples/wap/context/quiz/images/logo.wbmp
rename to tapestry-examples/wap/context/quiz/images/logo.wbmp
Binary files differ
diff --git a/examples/wap/jetty.xml b/tapestry-examples/wap/jetty.xml
similarity index 100%
rename from examples/wap/jetty.xml
rename to tapestry-examples/wap/jetty.xml
diff --git a/examples/wap/src/org/apache/tapestry/wap/hello/Hello.java b/tapestry-examples/wap/src/org/apache/tapestry/wap/hello/Hello.java
similarity index 100%
rename from examples/wap/src/org/apache/tapestry/wap/hello/Hello.java
rename to tapestry-examples/wap/src/org/apache/tapestry/wap/hello/Hello.java
diff --git a/examples/wap/src/org/apache/tapestry/wap/hello/Home.java b/tapestry-examples/wap/src/org/apache/tapestry/wap/hello/Home.java
similarity index 100%
rename from examples/wap/src/org/apache/tapestry/wap/hello/Home.java
rename to tapestry-examples/wap/src/org/apache/tapestry/wap/hello/Home.java
diff --git a/examples/wap/src/org/apache/tapestry/wap/quiz/Global.java b/tapestry-examples/wap/src/org/apache/tapestry/wap/quiz/Global.java
similarity index 100%
rename from examples/wap/src/org/apache/tapestry/wap/quiz/Global.java
rename to tapestry-examples/wap/src/org/apache/tapestry/wap/quiz/Global.java
diff --git a/examples/wap/src/org/apache/tapestry/wap/quiz/Home.java b/tapestry-examples/wap/src/org/apache/tapestry/wap/quiz/Home.java
similarity index 100%
rename from examples/wap/src/org/apache/tapestry/wap/quiz/Home.java
rename to tapestry-examples/wap/src/org/apache/tapestry/wap/quiz/Home.java
diff --git a/examples/wap/src/org/apache/tapestry/wap/quiz/Quiz.java b/tapestry-examples/wap/src/org/apache/tapestry/wap/quiz/Quiz.java
similarity index 100%
rename from examples/wap/src/org/apache/tapestry/wap/quiz/Quiz.java
rename to tapestry-examples/wap/src/org/apache/tapestry/wap/quiz/Quiz.java
diff --git a/examples/wap/src/org/apache/tapestry/wap/quiz/Scores.java b/tapestry-examples/wap/src/org/apache/tapestry/wap/quiz/Scores.java
similarity index 100%
rename from examples/wap/src/org/apache/tapestry/wap/quiz/Scores.java
rename to tapestry-examples/wap/src/org/apache/tapestry/wap/quiz/Scores.java
diff --git a/examples/wap/src/org/apache/tapestry/wap/quiz/Visit.java b/tapestry-examples/wap/src/org/apache/tapestry/wap/quiz/Visit.java
similarity index 100%
rename from examples/wap/src/org/apache/tapestry/wap/quiz/Visit.java
rename to tapestry-examples/wap/src/org/apache/tapestry/wap/quiz/Visit.java
diff --git a/tapestry-framework/.cvsignore b/tapestry-framework/.cvsignore
new file mode 100644
index 0000000..4b3be5f
--- /dev/null
+++ b/tapestry-framework/.cvsignore
@@ -0,0 +1,2 @@
+target
+classes
diff --git a/tapestry-framework/META-INF/taglib.tld b/tapestry-framework/META-INF/taglib.tld
new file mode 100644
index 0000000..77c1ae5
--- /dev/null
+++ b/tapestry-framework/META-INF/taglib.tld
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- $Id$ -->
+<!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN"
+	"http://java.sun.com/j2ee/dtds/web-jsptaglibrary_1_2.dtd">
+<taglib>
+	<tlib-version>1.0</tlib-version>
+	<jsp-version>1.2</jsp-version>
+	<short-name>tapestry</short-name>
+	<uri>http://jakarta.apache.org/tapestry/tld/tapestry_1_0.tld</uri>
+	<display-name>Tapestry</display-name>
+	<description>Tapestry JSP taglibrary for providing rudimentary access to a Tapestry application.</description>
+	<tag>
+		<name>page</name>
+		<tag-class>org.apache.tapestry.jsp.PageTag</tag-class>
+		<body-content>JSP</body-content>
+		<display-name>Page</display-name>
+		<description>Creates a link to a named page within a Tapestry
+			application.</description>
+		<attribute>
+			<name>servlet</name>
+			<required>no</required>
+			<rtexprvalue>yes</rtexprvalue>
+			<type>java.lang.String</type>
+			<description>The relative path to the servlet for the application.  The default value is "/app".</description>
+		</attribute>
+		<attribute>
+			<name>page</name>
+			<required>yes</required>
+			<rtexprvalue>yes</rtexprvalue>
+			<type>java.lang.String</type>
+			<description>The name of the page within the application.</description>
+		</attribute>
+		<attribute>
+			<name>styleClass</name>
+			<required>no</required>
+			<rtexprvalue>yes</rtexprvalue>
+			<type>java.lang.String</type>
+			<description>The CSS style class for the rendered tag.</description>
+		</attribute>
+	</tag>
+	<tag>
+		<name>page-url</name>
+		<tag-class>org.apache.tapestry.jsp.PageURLTag</tag-class>
+		<body-content>empty</body-content>
+		<display-name>Page URL</display-name>
+		<description>Inserts a URL to a named page within a Tapestry
+			application.</description>
+		<attribute>
+			<name>servlet</name>
+			<required>no</required>
+			<rtexprvalue>yes</rtexprvalue>
+			<type>java.lang.String</type>
+			<description>The relative path to the servlet for the application.  The default value is "/app".</description>
+		</attribute>
+		<attribute>
+			<name>page</name>
+			<required>yes</required>
+			<rtexprvalue>yes</rtexprvalue>
+			<type>java.lang.String</type>
+			<description>The name of the page within the application.</description>
+		</attribute>
+	</tag>
+	<tag>
+		<name>external</name>
+		<tag-class>org.apache.tapestry.jsp.ExternalTag</tag-class>
+		<body-content>JSP</body-content>
+		<display-name>External</display-name>
+		<description>Creates a link, with parameters, to an external page within a Tapestry application.</description>
+		<attribute>
+			<name>servlet</name>
+			<required>no</required>
+			<rtexprvalue>yes</rtexprvalue>
+			<type>java.lang.String</type>
+			<description>The relative path to the servlet for the application.  The default value is "/app".</description>
+		</attribute>
+		<attribute>
+			<name>page</name>
+			<required>yes</required>
+			<rtexprvalue>yes</rtexprvalue>
+			<type>java.lang.String</type>
+			<description>The name of the page within the application, which must be an external page.</description>
+		</attribute>
+		<attribute>
+			<name>styleClass</name>
+			<required>no</required>
+			<rtexprvalue>yes</rtexprvalue>
+			<type>java.lang.String</type>
+			<description>The CSS style class for the rendered tag.</description>
+		</attribute>
+		<attribute>
+			<name>parameters</name>
+			<required>no</required>
+			<rtexprvalue>yes</rtexprvalue>
+			<type>java.lang.String</type>
+			<description>Either a single string to pass as a parameter, or (if prefixed with "ognl:") an OGNL expression evaluated against the pageContext.</description>
+		</attribute>
+	</tag>
+	<tag>
+		<name>external-url</name>
+		<tag-class>org.apache.tapestry.jsp.ExternalURLTag</tag-class>
+		<body-content>empty</body-content>
+		<display-name>External URL</display-name>
+		<description>Inserts a URL to an external page within a Tapestry application, including service parameters.</description>
+		<attribute>
+			<name>servlet</name>
+			<required>no</required>
+			<rtexprvalue>yes</rtexprvalue>
+			<type>java.lang.String</type>
+			<description>The relative path to the servlet for the application.  The default value is "/app".</description>
+		</attribute>
+		<attribute>
+			<name>page</name>
+			<required>yes</required>
+			<rtexprvalue>yes</rtexprvalue>
+			<type>java.lang.String</type>
+			<description>The name of the page within the application, which must be an external page.</description>
+		</attribute>
+		<attribute>
+			<name>parameters</name>
+			<required>no</required>
+			<rtexprvalue>yes</rtexprvalue>
+			<type>java.lang.String</type>
+			<description>Either a single string to pass as a parameter, or (if prefixed with "ognl:") an OGNL expression evaluated against the pageContext.</description>
+		</attribute>
+	</tag>
+</taglib>
diff --git a/tapestry-framework/build.xml b/tapestry-framework/build.xml
new file mode 100644
index 0000000..052edc1
--- /dev/null
+++ b/tapestry-framework/build.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<!-- $Id$ -->
+<project name="Tapestry Framework" default="install">
+	<property name="root.dir" value=".."/>
+	<property file="${root.dir}/config/Version.properties"/>
+	<property file="${root.dir}/config/common.properties"/>
+	<property file="${root.dir}/config/build.properties"/>
+	
+	<path id="project.class.path">
+		<fileset dir="${root.lib.dir}">
+			<include name="${ext.dir}/*.jar"/>
+		</fileset>
+		<fileset dir="${root.lib.dir}">
+			<include name="${j2ee.dir}/*.jar"/>
+		</fileset>
+	</path>
+	<target name="init">
+		<mkdir dir="${classes.dir}"/>
+	</target>
+	<target name="clean">
+		<delete dir="${classes.dir}"/>
+	</target>
+
+	<target name="compile" depends="init"
+		description="Compile all classes in the framework.">
+		<javac srcdir="${src.dir}" destdir="${classes.dir}" debug="on"
+			target="1.1" source="1.3">
+			<classpath refid="project.class.path"/>
+		</javac>
+	</target>
+	<target name="install" depends="compile"
+		description="Compile all classes and build the installed JAR including all package resources."
+		>
+		<copy file="${root.dir}/config/Version.properties"
+			todir="${classes.dir}/org/apache/tapestry"/>
+		<jar jarfile="${root.lib.dir}/${framework.jar}">
+			<fileset dir="${classes.dir}">
+			</fileset>
+
+			<fileset dir="${src.dir}">
+				<exclude name="**/*.java"/>
+				<exclude name="**/package.html"/>
+			</fileset>
+			
+			<metainf dir="META-INF">
+				<include name="*"/>
+			</metainf>
+		</jar>
+
+	</target>
+</project>
diff --git a/tapestry-framework/pom.xml b/tapestry-framework/pom.xml
new file mode 100644
index 0000000..af23cc3
--- /dev/null
+++ b/tapestry-framework/pom.xml
@@ -0,0 +1,162 @@
+<!--suppress ALL -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.apache.tapestry</groupId>
+    <artifactId>tapestry-framework</artifactId>
+    <packaging>jar</packaging>
+    <version>3.0.5-SNAPSHOT</version>
+    <!-- This should change to tapestry-project -->
+    <parent>
+        <groupId>org.apache.tapestry</groupId>
+        <artifactId>tapestry-project</artifactId>
+        <version>3.0.5-SNAPSHOT</version>
+    </parent>
+    <name>Tapestry Core Library - ${version}</name>
+    <inceptionYear>2006</inceptionYear>
+
+    <dependencies>
+        <dependency>
+            <groupId>jdom</groupId>
+            <artifactId>jdom</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>jboss</groupId>
+            <artifactId>javassist</artifactId>
+            <!-- Override parent pom -->
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>ognl</groupId>
+            <artifactId>ognl</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>servlet-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>jdom</groupId>
+            <artifactId>jdom</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>log4j</groupId>
+            <artifactId>log4j</artifactId>
+            <version>1.2.13</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>commons-logging</groupId>
+            <artifactId>commons-logging</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-lang</groupId>
+            <artifactId>commons-lang</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-beanutils</groupId>
+            <artifactId>commons-beanutils</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-digester</groupId>
+            <artifactId>commons-digester</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>bsf</groupId>
+            <artifactId>bsf</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>oro</groupId>
+            <artifactId>oro</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-codec</groupId>
+            <artifactId>commons-codec</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-fileupload</groupId>
+            <artifactId>commons-fileupload</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <sourceDirectory>src</sourceDirectory>
+        <resources>
+            <resource>
+                <directory>META-INF</directory>
+                <includes>
+                    <include>**</include>
+                </includes>
+                <targetPath>META-INF</targetPath>
+            </resource>
+            <resource>
+                <directory>src</directory>
+                <includes>
+                    <include>**/*</include>
+                </includes>
+                <excludes>
+                    <exclude>**/*.java</exclude>
+                    <exclude>**/build.xml</exclude>
+                    <exclude>**/.cvsignore</exclude>
+                </excludes>
+            </resource>
+        </resources>
+        <!--
+                <testSourceDirectory>src/test</testSourceDirectory>
+                <testResources>
+                    <testResource>
+                        <directory>src/test</directory>
+                        <includes>
+                            <include>**/*</include>
+                        </includes>
+                        <excludes>
+                            <exclude>**/*.java</exclude>
+                        </excludes>
+                    </testResource>
+                    <testResource>
+                        <directory>src/conf</directory>
+                        <includes>
+                            <include>log4j.properties</include>
+                        </includes>
+                    </testResource>
+                </testResources>
+        -->
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>2.1</version>
+                <configuration>
+                    <archive>
+                        <compress>true</compress>
+                        <index>true</index>
+                    </archive>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-source-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <archive>
+                        <compress>true</compress>
+                        <index>true</index>
+                    </archive>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <reporting>
+        <outputDirectory>../target/site/tapestry-framework</outputDirectory>
+    </reporting>
+
+</project>
diff --git a/tapestry-framework/src/org/apache/tapestry/AbstractComponent.java b/tapestry-framework/src/org/apache/tapestry/AbstractComponent.java
new file mode 100644
index 0000000..4e406ec
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/AbstractComponent.java
@@ -0,0 +1,1146 @@
+//  Copyright 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.tapestry;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import ognl.OgnlRuntime;
+
+import org.apache.tapestry.bean.BeanProvider;
+import org.apache.tapestry.bean.BeanProviderPropertyAccessor;
+import org.apache.tapestry.engine.IPageLoader;
+import org.apache.tapestry.event.ChangeObserver;
+import org.apache.tapestry.event.PageDetachListener;
+import org.apache.tapestry.event.PageEvent;
+import org.apache.tapestry.event.PageRenderListener;
+import org.apache.tapestry.event.PageValidateListener;
+import org.apache.tapestry.listener.ListenerMap;
+import org.apache.tapestry.param.ParameterManager;
+import org.apache.tapestry.spec.BaseLocatable;
+import org.apache.tapestry.spec.IComponentSpecification;
+import org.apache.tapestry.util.prop.OgnlUtils;
+import org.apache.tapestry.util.prop.PropertyFinder;
+import org.apache.tapestry.util.prop.PropertyInfo;
+
+/**
+ *  Abstract base class implementing the {@link IComponent} interface.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public abstract class AbstractComponent extends BaseLocatable implements IComponent
+{
+    /**
+     * Used to check that subclasses invoke this implementation of prepareForRender().
+     * @see Tapestry#checkMethodInvocation(Object, String, Object)
+     * @since 3.0
+     */
+    private final static String PREPAREFORRENDER_METHOD_ID = "AbstractComponent.prepareForRender()";
+
+    /**
+     * Used to check that subclasses invoke this implementation of cleanupAfterRender().
+     * @see Tapestry#checkMethodInvocation(Object, String, Object)
+     * @since 3.0
+     */
+
+    private final static String CLEANUPAFTERRENDER_METHOD_ID =
+        "AbstractComponent.cleanupAfterRender()";
+
+    static {
+        // Register the BeanProviderHelper to provide access to the
+        // beans of a bean provider as named properties.
+
+        OgnlRuntime.setPropertyAccessor(IBeanProvider.class, new BeanProviderPropertyAccessor());
+    }
+
+    /**
+     *  The specification used to originally build the component.
+     *
+     * 
+     **/
+
+    private IComponentSpecification _specification;
+
+    /**
+     *  The page that contains the component, possibly itself (if the component is
+     *  in fact, a page).
+     *
+     * 
+     **/
+
+    private IPage _page;
+
+    /**
+     *  The component which contains the component.  This will only be
+     *  null if the component is actually a page.
+     *
+     **/
+
+    private IComponent _container;
+
+    /**
+     *  The simple id of this component.
+     *
+     * 
+     **/
+
+    private String _id;
+
+    /**
+     *  The fully qualified id of this component.  This is calculated the first time
+     *  it is needed, then cached for later.
+     *
+     **/
+
+    private String _idPath;
+
+    private static final int MAP_SIZE = 5;
+
+    /**
+     *  A {@link Map} of all bindings (for which there isn't a corresponding
+     *  JavaBeans property); the keys are the names of formal and informal
+     *  parameters.
+     *
+     **/
+
+    private Map _bindings;
+
+    private Map _components;
+    private static final int BODY_INIT_SIZE = 5;
+
+    private INamespace _namespace;
+
+    /**
+     *  Used in place of JDK 1.3's Collections.EMPTY_MAP (which is not
+     *  available in JDK 1.2).
+     *
+     **/
+
+    private static final Map EMPTY_MAP = Collections.unmodifiableMap(new HashMap(1));
+
+    /**
+     *  The number of {@link IRender} objects in the body of
+     *  this component.
+     *
+     * 
+     **/
+
+    private int _bodyCount = 0;
+
+    /**
+     *  An aray of elements in the body of this component.
+     *
+     * 
+     **/
+
+    private IRender[] _body;
+
+    /**
+     *  The components' asset map.
+     *
+     **/
+
+    private Map _assets;
+
+    /**
+     *  A mapping that allows public instance methods to be dressed up
+     *  as {@link IActionListener} listener
+     *  objects.
+     *
+     *  @since 1.0.2
+     * 
+     **/
+
+    private ListenerMap _listeners;
+
+    /**
+     *  A bean provider; these are lazily created as needed.
+     *
+     *  @since 1.0.4
+     * 
+     **/
+
+    private IBeanProvider _beans;
+
+    /**
+     *  Manages setting and clearing parameter properties for the component.
+     * 
+     *  @since 2.0.3
+     * 
+     **/
+
+    private ParameterManager _parameterManager;
+
+    /**
+     *  Provides access to localized Strings for this component.
+     * 
+     *  @since 2.0.4
+     * 
+     **/
+
+    private IMessages _strings;
+
+    public void addAsset(String name, IAsset asset)
+    {
+        if (_assets == null)
+            _assets = new HashMap(MAP_SIZE);
+
+        _assets.put(name, asset);
+    }
+
+    public void addComponent(IComponent component)
+    {
+        if (_components == null)
+            _components = new HashMap(MAP_SIZE);
+
+        _components.put(component.getId(), component);
+    }
+
+    /**
+     *  Adds an element (which may be static text or a component) as a body
+     *  element of this component.  Such elements are rendered
+     *  by {@link #renderBody(IMarkupWriter, IRequestCycle)}.
+     *
+     *  @since 2.2
+     * 
+     **/
+
+    public void addBody(IRender element)
+    {
+        // Should check the specification to see if this component
+        // allows body.  Curently, this is checked by the component
+        // in render(), which is silly.
+
+        if (_body == null)
+        {
+            _body = new IRender[BODY_INIT_SIZE];
+            _body[0] = element;
+
+            _bodyCount = 1;
+            return;
+        }
+
+        // No more room?  Make the array bigger.
+
+        if (_bodyCount == _body.length)
+        {
+            IRender[] newWrapped;
+
+            newWrapped = new IRender[_body.length * 2];
+
+            System.arraycopy(_body, 0, newWrapped, 0, _bodyCount);
+
+            _body = newWrapped;
+        }
+
+        _body[_bodyCount++] = element;
+    }
+
+    /**
+     *  Registers this component as a listener of the page if it
+     *  implements {@link org.apache.tapestry.event.PageDetachListener} or 
+     *  {@link org.apache.tapestry.event.PageRenderListener}.
+     * 
+     *  <p>
+     *  Invokes {@link #finishLoad()}.  Subclasses may overide as needed, but
+     *  must invoke this implementation.
+     *  {@link BaseComponent}
+     *  loads its HTML template. 
+     *
+     **/
+
+    public void finishLoad(
+        IRequestCycle cycle,
+        IPageLoader loader,
+        IComponentSpecification specification)
+    {
+        if (this instanceof PageDetachListener)
+            _page.addPageDetachListener((PageDetachListener) this);
+
+        if (this instanceof PageRenderListener)
+            _page.addPageRenderListener((PageRenderListener) this);
+
+        if (this instanceof PageValidateListener)
+            _page.addPageValidateListener((PageValidateListener) this);
+
+        finishLoad();
+    }
+
+    /**
+     * @deprecated To be removed in 3.1. 
+     * Use {@link Tapestry#fireObservedChange(IComponent, String, int)} instead.
+     */
+    protected void fireObservedChange(String propertyName, int newValue)
+    {
+        Tapestry.fireObservedChange(this, propertyName, newValue);
+    }
+
+    /**
+     * @deprecated To be removed in 3.1. 
+     * Use {@link Tapestry#fireObservedChange(IComponent, String, Object)} instead.
+     */
+    protected void fireObservedChange(String propertyName, Object newValue)
+    {
+        Tapestry.fireObservedChange(this, propertyName, newValue);
+    }
+
+    /**
+     * @deprecated To be removed in 3.1. 
+     * Use {@link Tapestry#fireObservedChange(IComponent, String, boolean)} instead.
+     */
+    protected void fireObservedChange(String propertyName, boolean newValue)
+    {
+        Tapestry.fireObservedChange(this, propertyName, newValue);
+    }
+
+    /**
+     * @deprecated To be removed in 3.1. 
+     * Use {@link Tapestry#fireObservedChange(IComponent, String, double)} instead.
+     */
+    protected void fireObservedChange(String propertyName, double newValue)
+    {
+        Tapestry.fireObservedChange(this, propertyName, newValue);
+    }
+
+    /**
+     * @deprecated To be removed in 3.1. 
+     * Use {@link Tapestry#fireObservedChange(IComponent, String, float)} instead.
+     */
+    protected void fireObservedChange(String propertyName, float newValue)
+    {
+        Tapestry.fireObservedChange(this, propertyName, newValue);
+    }
+
+    /**
+     * @deprecated To be removed in 3.1. 
+     * Use {@link Tapestry#fireObservedChange(IComponent, String, long)} instead.
+     */
+    protected void fireObservedChange(String propertyName, long newValue)
+    {
+        Tapestry.fireObservedChange(this, propertyName, newValue);
+    }
+
+    /**
+     * @deprecated To be removed in 3.1. 
+     * Use {@link Tapestry#fireObservedChange(IComponent, String, char)} instead.
+     */
+    protected void fireObservedChange(String propertyName, char newValue)
+    {
+        Tapestry.fireObservedChange(this, propertyName, newValue);
+    }
+
+    /**
+     * @deprecated To be removed in 3.1. 
+     * Use {@link Tapestry#fireObservedChange(IComponent, String, byte)} instead.
+     */
+    protected void fireObservedChange(String propertyName, byte newValue)
+    {
+        Tapestry.fireObservedChange(this, propertyName, newValue);
+    }
+
+    /**
+     * @deprecated To be removed in 3.1. 
+     * Use {@link Tapestry#fireObservedChange(IComponent, String, short)} instead.
+     */
+    protected void fireObservedChange(String propertyName, short newValue)
+    {
+        Tapestry.fireObservedChange(this, propertyName, newValue);
+    }
+
+    /**
+     *  @deprecated To be removed in 3.1.  Use 
+     *  {@link #renderInformalParameters(IMarkupWriter, IRequestCycle)}
+     *  instead.
+     * 
+     **/
+
+    protected void generateAttributes(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        renderInformalParameters(writer, cycle);
+    }
+
+    /**
+     *  Converts informal parameters into additional attributes on the
+     *  curently open tag.
+     *
+     *  <p>Invoked from subclasses to allow additional attributes to
+     *  be specified within a tag (this works best when there is a
+     *  one-to-one corespondence between an {@link IComponent} and a
+     *  HTML element.
+     *
+     *  <p>Iterates through the bindings for this component.  Filters
+     *  out bindings when the name matches a formal parameter (as of 1.0.5,
+     *  informal bindings are weeded out at page load / template load time,
+     *  if they match a formal parameter, or a specificied reserved name).
+     *  For the most part, all the bindings here are either informal parameter,
+     *  or formal parameter without a corresponding JavaBeans property.
+     *
+     *  <p>For each acceptible key, the value is extracted using {@link IBinding#getObject()}.
+     *  If the value is null, no attribute is written.
+     *
+     *  <p>If the value is an instance of {@link IAsset}, then
+     *  {@link IAsset#buildURL(IRequestCycle)} is invoked to convert the asset
+     *  to a URL.
+     *
+     *  <p>Finally, {@link IMarkupWriter#attribute(String,String)} is
+     *  invoked with the value (or the URL).
+     *
+     *  <p>The most common use for informal parameters is to support
+     *  the HTML class attribute (for use with cascading style sheets)
+     *  and to specify JavaScript event handlers.
+     *
+     *  <p>Components are only required to generate attributes on the
+     *  result phase; this can be skipped during the rewind phase.
+     **/
+
+    protected void renderInformalParameters(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        String attribute;
+
+        if (_bindings == null)
+            return;
+
+        Iterator i = _bindings.entrySet().iterator();
+
+        while (i.hasNext())
+        {
+            Map.Entry entry = (Map.Entry) i.next();
+            String name = (String) entry.getKey();
+
+            IBinding binding = (IBinding) entry.getValue();
+
+            Object value = binding.getObject();
+            if (value == null)
+                continue;
+
+            if (value instanceof IAsset)
+            {
+                IAsset asset = (IAsset) value;
+
+                // Get the URL of the asset and insert that.
+
+                attribute = asset.buildURL(cycle);
+            }
+            else
+                attribute = value.toString();
+
+            writer.attribute(name, attribute);
+        }
+
+    }
+
+    /**
+     *  Returns an object used to resolve classes.
+     *  @since 3.0
+     *
+     **/
+    private IResourceResolver getResourceResolver()
+    {
+        return getPage().getEngine().getResourceResolver();
+    }
+
+	/**
+	 *  Returns the named binding, or null if it doesn't exist.
+	 *
+	 *  <p>This method looks for a JavaBeans property with an
+	 *  appropriate name, of type {@link IBinding}.  The property
+	 *  should be named <code><i>name</i>Binding</code>.  If it exists
+	 *  and is both readable and writable, then it is accessor method
+	 *  is invoked.  Components which implement such methods can
+	 *  access their own binding through their instance variables
+	 *  instead of invoking this method, a performance optimization.
+	 *
+	 *  @see #setBinding(String,IBinding)
+	 *
+	 **/
+
+	public IBinding getBinding(String name)
+	{
+		String bindingPropertyName = name + Tapestry.PARAMETER_PROPERTY_NAME_SUFFIX;
+		PropertyInfo info = PropertyFinder.getPropertyInfo(getClass(), bindingPropertyName);
+
+		if (info != null && info.isReadWrite() && info.getType().equals(IBinding.class))
+		{
+			IResourceResolver resolver = getPage().getEngine().getResourceResolver();
+
+			return (IBinding) OgnlUtils.get(bindingPropertyName, resolver, this);
+		}
+
+		if (_bindings == null)
+			return null;
+
+		return (IBinding) _bindings.get(name);
+	}
+
+
+    /**
+     *  Return's the page's change observer.  In practical terms, this
+     *  will be an {@link org.apache.tapestry.engine.IPageRecorder}.
+     *
+     *  @see IPage#getChangeObserver()
+     *  @deprecated To be removed in 3.1; use {@link IPage#getChangeObserver()}.
+     **/
+
+    public ChangeObserver getChangeObserver()
+    {
+        return _page.getChangeObserver();
+    }
+
+    public IComponent getComponent(String id)
+    {
+        IComponent result = null;
+
+        if (_components != null)
+            result = (IComponent) _components.get(id);
+
+        if (result == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.format("no-such-component", this, id),
+                this,
+                null,
+                null);
+
+        return result;
+    }
+
+    public IComponent getContainer()
+    {
+        return _container;
+    }
+
+    public void setContainer(IComponent value)
+    {
+        if (_container != null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("AbstractComponent.attempt-to-change-container"));
+
+        _container = value;
+    }
+
+    /**
+     *  Returns the name of the page, a slash, and this component's id path.
+     *  Pages are different, they simply return their name.
+     *
+     *  @see #getIdPath()
+     *
+     **/
+
+    public String getExtendedId()
+    {
+        if (_page == null)
+            return null;
+
+        return _page.getPageName() + "/" + getIdPath();
+    }
+
+    public String getId()
+    {
+        return _id;
+    }
+
+    public void setId(String value)
+    {
+        if (_id != null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("AbstractComponent.attempt-to-change-component-id"));
+
+        _id = value;
+    }
+
+    public String getIdPath()
+    {
+        String containerIdPath;
+
+        if (_container == null)
+            throw new NullPointerException(
+                Tapestry.format("AbstractComponent.null-container", this));
+
+        containerIdPath = _container.getIdPath();
+
+        if (containerIdPath == null)
+            _idPath = _id;
+        else
+            _idPath = containerIdPath + "." + _id;
+
+        return _idPath;
+    }
+
+    public IPage getPage()
+    {
+        return _page;
+    }
+
+    public void setPage(IPage value)
+    {
+        if (_page != null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("AbstractComponent.attempt-to-change-page"));
+
+        _page = value;
+    }
+
+    public IComponentSpecification getSpecification()
+    {
+        return _specification;
+    }
+
+    public void setSpecification(IComponentSpecification value)
+    {
+        if (_specification != null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("AbstractComponent.attempt-to-change-spec"));
+
+        _specification = value;
+    }
+
+    /**
+     *  Renders all elements wrapped by the receiver.
+     *
+     **/
+
+    public void renderBody(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        for (int i = 0; i < _bodyCount; i++)
+            _body[i].render(writer, cycle);
+    }
+
+	/**
+	 *  Adds the binding with the given name, replacing any existing binding
+	 *  with that name.
+	 *
+	 *  <p>This method checks to see if a matching JavaBeans property
+	 *  (with a name of <code><i>name</i>Binding</code> and a type of
+	 *  {@link IBinding}) exists.  If so, that property is updated.
+	 *  An optimized component can simply implement accessor and
+	 *  mutator methods and then access its bindings via its own
+	 *  instance variables, rather than going through {@link
+	 *  #getBinding(String)}.
+	 *
+	 *  <p>Informal parameters should <em>not</em> be stored in
+	 *  instance variables if {@link
+	 *  #renderInformalParameters(IMarkupWriter, IRequestCycle)} is to be used.
+	 *  It relies on using the collection of bindings (to store informal parameters).
+	 **/
+
+	public void setBinding(String name, IBinding binding)
+	{
+		String bindingPropertyName = name + Tapestry.PARAMETER_PROPERTY_NAME_SUFFIX;
+
+		PropertyInfo info = PropertyFinder.getPropertyInfo(getClass(), bindingPropertyName);
+
+		if (info != null && info.isReadWrite() && info.getType().equals(IBinding.class))
+		{
+			IResourceResolver resolver = getPage().getEngine().getResourceResolver();
+			OgnlUtils.set(bindingPropertyName, resolver, this, binding);
+			return;
+		}
+
+		if (_bindings == null)
+			_bindings = new HashMap(MAP_SIZE);
+
+		_bindings.put(name, binding);
+	}
+
+
+    public String toString()
+    {
+        StringBuffer buffer;
+
+        buffer = new StringBuffer(super.toString());
+
+        buffer.append('[');
+
+        buffer.append(getExtendedId());
+
+        buffer.append(']');
+
+        return buffer.toString();
+    }
+
+    /**
+     *  Returns an unmodifiable {@link Map} of components, keyed on component id.
+     *  Never returns null, but may return an empty map.  The returned map is
+     *  immutable.
+     *
+     **/
+
+    public Map getComponents()
+    {
+        if (_components == null)
+            return EMPTY_MAP;
+
+        return Collections.unmodifiableMap(_components);
+
+    }
+
+    public Map getAssets()
+    {
+        if (_assets == null)
+            return EMPTY_MAP;
+
+        return Collections.unmodifiableMap(_assets);
+    }
+
+    public IAsset getAsset(String name)
+    {
+        if (_assets == null)
+            return null;
+
+        return (IAsset) _assets.get(name);
+    }
+
+    public Collection getBindingNames()
+    {
+        // If no conainer, i.e. a page, then no bindings.
+
+        if (_container == null)
+            return null;
+
+        HashSet result = new HashSet();
+
+        // All the informal bindings go into the bindings Map. 
+
+        if (_bindings != null)
+            result.addAll(_bindings.keySet());
+
+        // Now, iterate over the formal parameters and add the formal parameters
+        // that have a binding.
+
+        List names = _specification.getParameterNames();
+
+        int count = names.size();
+
+        for (int i = 0; i < count; i++)
+        {
+            String name = (String) names.get(i);
+
+            if (result.contains(name))
+                continue;
+
+            if (getBinding(name) != null)
+                result.add(name);
+        }
+
+        return result;
+    }
+
+    /** 
+     *
+     *  Returns a {@link Map} of all bindings for this component.  This implementation
+     *  is expensive, since it has to merge the disassociated bindings (informal parameters,
+     *  and parameters without a JavaBeans property) with the associated bindings (formal
+     *  parameters with a JavaBeans property).
+     *
+     * @since 1.0.5
+     *
+     **/
+
+    public Map getBindings()
+    {
+        Map result = new HashMap();
+
+        // Add any informal parameters.
+
+        if (_bindings != null)
+            result.putAll(_bindings);
+
+        // Now work on the formal parameters
+
+        Iterator i = _specification.getParameterNames().iterator();
+        while (i.hasNext())
+        {
+            String name = (String) i.next();
+
+            if (result.containsKey(name))
+                continue;
+
+            IBinding binding = getBinding(name);
+
+            if (binding != null)
+                result.put(name, binding);
+        }
+
+        return result;
+    }
+
+    /**
+     *  Returns a {@link ListenerMap} for the component.  A {@link ListenerMap} contains a number of
+     *  synthetic read-only properties that implement the {@link IActionListener} 
+     *  interface, but in fact, cause public instance methods to be invoked.
+     *
+     *  @since 1.0.2
+     **/
+
+    public ListenerMap getListeners()
+    {
+        if (_listeners == null)
+            _listeners = new ListenerMap(this);
+
+        return _listeners;
+    }
+
+    /**
+     *  Returns the {@link IBeanProvider} for this component.  This is lazily created the
+     *  first time it is needed.
+     *
+     *  @since 1.0.4
+     *
+     **/
+
+    public IBeanProvider getBeans()
+    {
+        if (_beans == null)
+            _beans = new BeanProvider(this);
+
+        return _beans;
+    }
+
+    /**
+     * 
+     *  Invoked, as a convienience, 
+     *  from {@link #finishLoad(IRequestCycle, IPageLoader, IComponentSpecification)}.
+     *  This implemenation does nothing.  Subclasses may override without invoking
+     *  this implementation.
+     * 
+     *  @since 1.0.5
+     *
+     **/
+
+    protected void finishLoad()
+    {
+    }
+
+    /**
+     *  The main method used to render the component.  
+     *  Invokes {@link #prepareForRender(IRequestCycle)}, then
+     *  {@link #renderComponent(IMarkupWriter, IRequestCycle)}.
+     *  {@link #cleanupAfterRender(IRequestCycle)} is invoked in a 
+     *  <code>finally</code> block.
+     * 	 
+     *  <p>Subclasses should not override this method; instead they
+     *  will implement {@link #renderComponent(IMarkupWriter, IRequestCycle)}.
+     * 
+     *  @since 2.0.3
+     * 
+     **/
+
+    public final void render(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        try
+        {
+            Tapestry.clearMethodInvocations();
+
+            prepareForRender(cycle);
+
+            Tapestry.checkMethodInvocation(PREPAREFORRENDER_METHOD_ID, "prepareForRender()", this);
+
+            renderComponent(writer, cycle);
+        }
+        finally
+        {
+            Tapestry.clearMethodInvocations();
+
+            cleanupAfterRender(cycle);
+
+            Tapestry.checkMethodInvocation(
+                CLEANUPAFTERRENDER_METHOD_ID,
+                "cleanupAfterRender()",
+                this);
+        }
+    }
+
+    /**
+     *  Invoked by {@link #render(IMarkupWriter, IRequestCycle)}
+     *  to prepare the component to render.  This implementation
+     *  sets JavaBeans properties from matching bound parameters.
+     *  Subclasses that override this method must invoke this
+     *  implementation as well.
+     * 
+     *  @since 2.0.3
+     * 
+     **/
+
+    protected void prepareForRender(IRequestCycle cycle)
+    {
+        Tapestry.addMethodInvocation(PREPAREFORRENDER_METHOD_ID);
+
+        if (_parameterManager == null)
+        {
+            // Pages inherit from this class too, but pages (by definition)
+            // never have parameters.
+
+            if (getSpecification().isPageSpecification())
+                return;
+
+            _parameterManager = new ParameterManager(this);
+        }
+
+        _parameterManager.setParameters(cycle);
+    }
+
+    /**
+     *  Invoked by {@link #render(IMarkupWriter, IRequestCycle)}
+     *  to actually render the component (with any parameter values
+     *  already set).  This is the method that subclasses must implement.
+     * 
+     *  @since 2.0.3
+     * 
+     **/
+
+    protected abstract void renderComponent(IMarkupWriter writer, IRequestCycle cycle);
+
+    /**
+     *  Invoked by {@link #render(IMarkupWriter, IRequestCycle)}
+     *  after the component renders, to clear any parameters back to
+     *  null (or 0, or false, or whatever the correct default is).  
+     *  Primarily, this is used to ensure
+     *  that the component doesn't hold onto any objects that could
+     *  otherwise be garbage collected.
+     * 
+     *  <p>Subclasses may override this implementation, but must
+     *  also invoke it.
+     * 
+     *  @since 2.0.3
+     * 
+     **/
+
+    protected void cleanupAfterRender(IRequestCycle cycle)
+    {
+        Tapestry.addMethodInvocation(CLEANUPAFTERRENDER_METHOD_ID);
+
+        if (_parameterManager != null)
+            _parameterManager.resetParameters(cycle);
+    }
+
+    /** @since 3.0 **/
+
+    public IMessages getMessages()
+    {
+        if (_strings == null)
+            _strings = getPage().getEngine().getComponentMessagesSource().getMessages(this);
+
+        return _strings;
+    }
+
+    /**
+     *  Obtains the {@link IMessages} for this component
+     *  (if necessary), and gets the string from it.
+     * 
+     **/
+
+    public String getString(String key)
+    {
+        return getMessages().getMessage(key);
+    }
+
+    public String getMessage(String key)
+    {
+        // Invoke the deprecated implementation (for code coverage reasons).
+        // In 3.1, remove getString() and move its implementation
+        // here.
+
+        return getString(key);
+    }
+
+    /**
+     *  Formats a message string, using
+     *  {@link IMessages#format(java.lang.String, java.lang.Object[])}.
+     * 
+     *  @param key the key used to obtain a localized pattern using
+     *  {@link #getString(String)}
+     *  @param arguments passed to the formatter
+     * 
+     *  @since 2.2
+     *  @deprecated To be removed in 3.1.  Use {@link #format(String, Object[])} instead.
+     **/
+
+    public String formatString(String key, Object[] arguments)
+    {
+        return getMessages().format(key, arguments);
+    }
+
+    /**
+     * Formats a localized message string, using
+     * {@link IMessages#format(java.lang.String, java.lang.Object[])}.
+     *
+     * @param key the key used to obtain a localized pattern using
+     * {@link #getString(String)}
+     * @param arguments passed to the formatter
+     * 
+     * @since 3.0
+     */
+
+    public String format(String key, Object[] arguments)
+    {
+        // SOP: New name invokes deprecated method (consistency and 
+        // code coverage); in 3.1 we move the implementation here.
+
+        return formatString(key, arguments);
+    }
+
+    /**
+     *  Convienience method for invoking {@link IMessages#format(String, Object[])}
+     * 
+     *  @since 2.2
+     *  @deprecated To be removed in 3.1.  Use {@link #format(String, Object)} instead.
+     * 
+     **/
+
+    public String formatString(String key, Object argument)
+    {
+        return getMessages().format(key, argument);
+    }
+
+    /**
+     * Convienience method for invoking {@link IMessages#format(String, Object)}
+     * 
+     * @since 3.0
+     * 
+     */
+
+    public String format(String key, Object argument)
+    {
+        return formatString(key, argument);
+    }
+
+    /**
+     *  Convienience method for invoking {@link IMessages#format(String, Object, Object)}.
+     * 
+     *  @since 2.2
+     *  @deprecated To be removed in 3.1.  Use {@link #format(String, Object, Object)} instead.
+     **/
+
+    public String formatString(String key, Object argument1, Object argument2)
+    {
+        return getMessages().format(key, argument1, argument2);
+    }
+
+    /**
+     * Convienience method for invoking {@link IMessages#format(String, Object, Object)}.
+     * 
+     * @since 3.0
+     * 
+     **/
+
+    public String format(String key, Object argument1, Object argument2)
+    {
+        return formatString(key, argument1, argument2);
+    }
+
+    /**
+     * Convienience method for {@link IMessages#format(String, Object, Object, Object)}.
+     * 
+     * @since 2.2
+     * @deprecated To be removed in 3.1.  Use {@link #format(String, Object, Object, Object)} instead.
+     */
+
+    public String formatString(String key, Object argument1, Object argument2, Object argument3)
+    {
+        return getMessages().format(key, argument1, argument2, argument3);
+    }
+
+    /**
+     * Convienience method for {@link IMessages#format(String, Object, Object, Object)}.
+     * 
+     * @since 3.0
+     */
+
+    public String format(String key, Object argument1, Object argument2, Object argument3)
+    {
+        return formatString(key, argument1, argument2, argument3);
+    }
+
+    public INamespace getNamespace()
+    {
+        return _namespace;
+    }
+
+    public void setNamespace(INamespace namespace)
+    {
+        _namespace = namespace;
+    }
+
+    /**
+     *  Returns the body of the component, the element (which may be static HTML or components)
+     *  that the component immediately wraps.  May return null.  Do not modify the returned
+     *  array.  The array may be padded with nulls.
+     * 
+     *  @since 2.3
+     *  @see #getBodyCount()
+     * 
+     **/
+
+    public IRender[] getBody()
+    {
+        return _body;
+    }
+
+    /**
+     *  Returns the active number of elements in the the body, which may be zero.
+     * 
+     *  @since 2.3
+     *  @see #getBody()
+     * 
+     **/
+
+    public int getBodyCount()
+    {
+        return _bodyCount;
+    }
+
+    /**
+     * Empty implementation of
+     * {@link org.apache.tapestry.event.PageRenderListener#pageEndRender(PageEvent)}.
+     * This allows classes to implement
+     * {@link org.apache.tapestry.event.PageRenderListener} and only
+     * implement the
+     * {@link org.apache.tapestry.event.PageRenderListener#pageBeginRender(PageEvent)}
+     * method.
+     * @since 3.0
+     */
+
+    public void pageEndRender(PageEvent event)
+    {
+    }
+
+    /**
+     *  Sets a property of a component.
+     *  @see IComponent 
+     *  @since 3.0
+     */
+    public void setProperty(String propertyName, Object value)
+    {
+        IResourceResolver resolver = getResourceResolver();
+        OgnlUtils.set(propertyName, resolver, this, value);
+    }
+    /**
+     *  Gets a property of a component.
+     *  @see IComponent 
+     *  @since 3.0
+     */
+    public Object getProperty(String propertyName)
+    {
+        IResourceResolver resolver = getResourceResolver();
+        return OgnlUtils.get(propertyName, resolver, this);
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/AbstractMarkupWriter.java b/tapestry-framework/src/org/apache/tapestry/AbstractMarkupWriter.java
new file mode 100644
index 0000000..b4514b3
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/AbstractMarkupWriter.java
@@ -0,0 +1,843 @@
+//  Copyright 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.tapestry;
+
+import java.io.BufferedWriter;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.util.Stack;
+
+import org.apache.tapestry.util.ContentType;
+
+/**
+ * Abstract base class implementing the {@link IMarkupWriter} interface.
+ * This class is used to create a Generic Tag Markup Language (GTML) output.   
+ * It is more sophisticated than <code>PrintWriter</code> in that it maintains   
+ * a concept hierarchy of open GTML tags. It also supplies a number of other
+ * of the features that are useful when creating GTML.
+ *
+ * Elements are started with the {@link #begin(String)} 
+ * or {@link #beginEmpty(String)}
+ * methods. Once they are started, attributes for the elements may be set with
+ * the various <code>attribute()</code> methods. The element is closed off
+ * (i.e., the closing '&gt;' character is written) when any other method
+ * is invoked (exception: methods which do not produce output, such as
+ * {@link #flush()}). The <code>end()</code> methods end an element,
+ * writing an GTML close tag to the output.
+ *
+ * <p>TBD:
+ * <ul>
+ * <li>Support XML and XHTML
+ *  <li>What to do with Unicode characters with a value greater than 255?
+ * </ul>
+ *
+ * <p>This class is derived from the original class 
+ * <code>com.primix.servlet.HTMLWriter</code>,
+ * part of the <b>ServletUtils</b> framework available from
+ * <a href="http://www.gjt.org/servlets/JCVSlet/list/gjt/com/primix/servlet">The Giant 
+ * Java Tree</a>.
+ *
+ * @version $Id$
+ * @author Howard Ship, David Solis
+ * @since 0.2.9
+ *
+ **/
+
+public abstract class AbstractMarkupWriter implements IMarkupWriter
+{
+    /**
+     * The encoding to be used should it be omitted in the constructors.
+     * This is only used for backward compatibility. New code always provides the encoding.
+     */
+
+    private static final String DEFAULT_ENCODING = "utf-8";
+
+    /**
+     * The underlying {@link PrintWriter} that output is sent to. 
+     *  
+     **/
+
+    private PrintWriter _writer;
+
+    /**
+     * Indicates whether a tag is open or not. A tag is opened by
+     * {@link #begin(String)} or {@link #beginEmpty(String)}.
+     * It stays open while calls to the <code>attribute()</code>
+     * methods are made. It is closed
+     * (the '&gt;' is written) when any other method is invoked.
+     *
+     **/
+
+    private boolean _openTag = false;
+
+    /**
+     *  Indicates that the tag was opened with 
+     *  {@link #beginEmpty(String)}, which affects
+     *  how the tag is closed (a slash is added to indicate the
+     *  lack of a body).  This is compatible with HTML, but reflects
+     *  an XML/XHTML leaning.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    private boolean _emptyTag = false;
+
+    /**
+     * A Stack of Strings used to track the active tag elements. Elements are active
+     * until the corresponding close tag is written.  The {@link #push(String)} method
+     * adds elements to the stack, {@link #pop()} removes them.
+     *
+     **/
+
+    private Stack _activeElementStack;
+
+    /**
+     * The depth of the open tag stack.
+     * @see #_activeElementStack
+     *
+     **/
+
+    private int _depth = 0;
+
+    private char[] _buffer;
+
+    private String[] _entities;
+    private boolean[] _safe;
+
+    /**
+     *  Implemented in concrete subclasses to provide an indication of which
+     *  characters are 'safe' to insert directly into the response.  The index
+     *  into the array is the character, if the value at the index is false (or the
+     *  index out of range), then the character is escaped.
+     *
+     **/
+
+    private String _contentType;
+
+    /**
+     *  Indicates whether {@link #close()} should close the 
+     *  underlying {@link PrintWriter}.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    private boolean _propagateClose = true;
+
+    public String getContentType()
+    {
+        return _contentType;
+    }
+
+    abstract public IMarkupWriter getNestedWriter();
+
+    /**
+     *  General constructor used by subclasses.
+     * 
+     *  @param safe an array of flags indicating which characters
+     *  can be passed directly through with out filtering.  Characters marked
+     *  unsafe, or outside the range defined by safe, are converted to entities.
+     *  @param entities a set of prefered entities, unsafe characters with
+     *  a defined entity use the entity, other characters are converted
+     *  to numeric entities.
+     *  @param contentType the MIME type of the content produced by the writer.
+     *  @param encoding the encoding of content produced by the writer.
+     *  @param stream stream to which content will be written.
+     *
+     **/
+
+    protected AbstractMarkupWriter(
+        boolean safe[],
+        String[] entities,
+        String contentType,
+        String encoding,
+        OutputStream stream)
+    {
+        if (entities == null || safe == null || contentType == null || encoding == null)
+            throw new IllegalArgumentException(
+                Tapestry.getMessage("AbstractMarkupWriter.missing-constructor-parameters"));
+
+        _entities = entities;
+        _safe = safe;
+
+        _contentType = generateFullContentType(contentType, encoding);
+
+        setOutputStream(stream, encoding);
+    }
+
+    /**
+     *  General constructor used by subclasses.
+     *  This constructor is left for backward compatibility. It is preferred that 
+     *  it is not used since it does not specify an encoding for conversion.
+     * 
+     *  @param safe an array of flags indicating which characters
+     *  can be passed directly through with out filtering.  Characters marked
+     *  unsafe, or outside the range defined by safe, are converted to entities.
+     *  @param entities a set of prefered entities, unsafe characters with
+     *  a defined entity use the entity, other characters are converted
+     *  to numeric entities.
+     *  @param contentType the type of content produced by the
+     *  writer.
+     *  @param stream stream to which content will be written.
+     **/
+
+    protected AbstractMarkupWriter(
+        boolean safe[],
+        String[] entities,
+        String contentType,
+        OutputStream stream)
+    {
+        this(safe, entities, contentType);
+
+        ContentType contentTypeObject = new ContentType(contentType);
+        String encoding = contentTypeObject.getParameter("charset");
+
+        setOutputStream(stream, encoding);
+    }
+
+    /**
+     *  Creates new markup writer around the underlying {@link PrintWriter}.
+     * 
+     *  <p>This is primarily used by {@link org.apache.tapestry.engine.TagSupportService},
+     *  which is inlcuding content, and therefore this method will not
+     *  close the writer when the markup writer is closed.
+     * 
+     *  @since 3.0
+     *  
+     **/
+
+    protected AbstractMarkupWriter(
+        boolean safe[],
+        String[] entities,
+        String contentType,
+        PrintWriter writer)
+    {
+        this(safe, entities, contentType);
+
+        // When the markup writer is closed, the underlying writer
+        // is NOT closed.
+
+        _propagateClose = false;
+        _writer = writer;
+    }
+
+    /**
+     *  Special constructor used for nested response writers.
+     *  The subclass is responsible for creating the writer.
+     * 
+     **/
+
+    protected AbstractMarkupWriter(boolean safe[], String[] entities, String contentType)
+    {
+        if (entities == null || safe == null || contentType == null)
+            throw new IllegalArgumentException(
+                Tapestry.getMessage("AbstractMarkupWriter.missing-constructor-parameters"));
+
+        _entities = entities;
+        _safe = safe;
+        _contentType = generateFullContentType(contentType, DEFAULT_ENCODING);
+    }
+
+    /**
+     * Ensures that the content type has a charset (encoding) parameter.
+     * 
+     * @param contentType The content type, e.g. text/html. It may contain a charset parameter.
+     * @param encoding The value of the charset parameter of the content type if it is not already present.
+     * @return The content type containing a charset parameter, e.g. text/html;charset=utf-8 
+     */
+    private String generateFullContentType(String contentType, String encoding)
+    {
+        ContentType contentTypeObject = new ContentType(contentType);
+        if (contentTypeObject.getParameter("charset") == null)
+            contentTypeObject.setParameter("charset", encoding);
+        return contentTypeObject.unparse();
+    }
+
+    protected void setWriter(PrintWriter writer)
+    {
+        _writer = writer;
+    }
+
+    protected void setOutputStream(OutputStream stream, String encoding)
+    {
+        try
+        {
+            OutputStreamWriter owriter;
+            if (encoding != null)
+                owriter = new OutputStreamWriter(stream, encoding);
+            else
+                owriter = new OutputStreamWriter(stream);
+            Writer bwriter = new BufferedWriter(owriter);
+
+            _writer = new PrintWriter(bwriter);
+        }
+        catch (UnsupportedEncodingException e)
+        {
+            throw new IllegalArgumentException(
+                Tapestry.format("illegal-encoding", encoding));
+        }
+    }
+
+    /**
+     * Writes an integer attribute into the currently open tag.
+     *
+     * <p>TBD: Validate that name is legal.
+     *
+     * @throws IllegalStateException if there is no open tag.
+     *
+     **/
+
+    public void attribute(String name, int value)
+    {
+        checkTagOpen();
+
+        _writer.print(' ');
+        _writer.print(name);
+        _writer.print("=\"");
+        _writer.print(value);
+        _writer.print('"');
+    }
+
+    /**
+     *  Writes a boolean attribute into the currently open tag.
+     *
+     *  <p>TBD: Validate that name is legal.
+     *
+     *  @throws IllegalStateException if there is no open tag.
+     *
+     *  @since 3.0
+     *
+     **/
+
+    public void attribute(String name, boolean value)
+    {
+        checkTagOpen();
+
+        _writer.print(' ');
+        _writer.print(name);
+        _writer.print("=\"");
+        _writer.print(value);
+        _writer.print('"');
+    }
+
+    /**
+     *  Writes an attribute into the most recently opened tag. This must be called after
+     *  {@link #begin(String)}
+     *  and before any other kind of writing (which closes the tag).
+     *
+     *  <p>The value may be null. A null value will be rendered as an empty string.
+     *
+     *  <p>Troublesome characters in the value are converted to thier GTML entities, much
+     *  like a <code>print()</code> method, with the following exceptions:
+     *  <ul>
+     *  <li>The double quote (&quot;) is converted to &amp;quot;
+     *  <li>The ampersand (&amp;) is passed through unchanged
+     *  </ul>
+     *
+     *  @throws IllegalStateException if there is no open tag.
+     *  @param name The name of the attribute to write (no validation
+     *  is done on the name).
+     *  @param value The value to write.  If null, the attribute
+     *  name is written as the value.  Otherwise, the
+     *  value is written, 
+     **/
+
+    public void attribute(String name, String value)
+    {
+        checkTagOpen();
+
+        _writer.print(' ');
+
+        // Could use a check here that name contains only valid characters
+
+        _writer.print(name);
+        _writer.print("=\"");
+
+        if (value != null)
+        {
+            int length = value.length();
+
+            if (_buffer == null || _buffer.length < length)
+                _buffer = new char[length];
+
+            value.getChars(0, length, _buffer, 0);
+
+            safePrint(_buffer, 0, length, true);
+        }
+
+        _writer.print('"');
+
+    }
+
+    /**
+      *  Similar to {@link #attribute(String, String)} but no escaping of invalid elements
+      *  is done for the value.
+      * 
+      *  @throws IllegalStateException if there is no open tag.
+      *
+      *  @since 3.0
+      *
+      **/
+    public void attributeRaw(String name, String value)
+    {
+        if (value == null)
+        {
+            attribute(name, value);
+            return;
+        }
+
+        checkTagOpen();
+
+        _writer.print(' ');
+
+        _writer.print(name);
+
+        _writer.print("=\"");
+        _writer.print(value);
+        _writer.print('"');
+    }
+
+    /**
+     * Closes any existing tag then starts a new element. The new element is pushed
+     * onto the active element stack.
+     **/
+
+    public void begin(String name)
+    {
+        if (_openTag)
+            closeTag();
+
+        push(name);
+
+        _writer.print('<');
+        _writer.print(name);
+
+        _openTag = true;
+        _emptyTag = false;
+    }
+
+    /**
+     * Starts an element that will not later be matched with an <code>end()</code>
+     * call. This is useful for elements such as &lt;hr;&gt; or &lt;br&gt; that
+     * do not need closing tags.
+     *
+     **/
+
+    public void beginEmpty(String name)
+    {
+        if (_openTag)
+            closeTag();
+
+        _writer.print('<');
+        _writer.print(name);
+
+        _openTag = true;
+        _emptyTag = true;
+    }
+
+    /**
+     * Invokes <code>checkError()</code> on the
+     *  <code>PrintWriter</code> used to format output.
+     **/
+
+    public boolean checkError()
+    {
+        return _writer.checkError();
+    }
+
+    private void checkTagOpen()
+    {
+        if (!_openTag)
+            throw new IllegalStateException(
+                Tapestry.getMessage("AbstractMarkupWriter.tag-not-open"));
+    }
+
+    /**
+     * Closes this <code>IMarkupWriter</code>. Any active elements are closed. The
+     * {@link PrintWriter} is then  sent {@link PrintWriter#close()}.
+     *
+     **/
+
+    public void close()
+    {
+        if (_openTag)
+            closeTag();
+
+        // Close any active elements.
+
+        while (_depth > 0)
+        {
+            _writer.print("</");
+            _writer.print(pop());
+            _writer.print('>');
+        }
+
+        if (_propagateClose)
+            _writer.close();
+
+        _writer = null;
+        _activeElementStack = null;
+        _buffer = null;
+    }
+
+    /**
+     *  Closes the most recently opened element by writing the '&gt;' that ends
+     *  it. May write a slash before the '&gt;' if the tag
+     *  was opened by {@link #beginEmpty(String)}.
+     * 
+     *  <p>Once this is invoked, the <code>attribute()</code> methods
+     *  may not be used until a new element is opened with {@link #begin(String)} or
+     *  or {@link #beginEmpty(String)}.
+     **/
+
+    public void closeTag()
+    {
+        if (_emptyTag)
+            _writer.print('/');
+
+        _writer.print('>');
+
+        _openTag = false;
+        _emptyTag = false;
+    }
+
+    /**
+     * Writes an GTML comment. Any open tag is first closed. 
+     * The method takes care of
+     * providing the <code>&lt;!--</code> and <code>--&gt;</code>, 
+     * including a blank line after the close of the comment.
+     *
+     * <p>Most characters are valid inside an GTML comment, so no check
+     * of the contents is made (much like {@link #printRaw(String)}.
+     *
+     **/
+
+    public void comment(String value)
+    {
+        if (_openTag)
+            closeTag();
+
+        _writer.print("<!-- ");
+        _writer.print(value);
+        _writer.println(" -->");
+    }
+
+    /**
+     * Ends the element most recently started by {@link #begin(String)}. 
+     * The name of the tag
+     * is popped off of the active element stack and used to form an GTML close tag.
+     *
+     * <p>TBD: Error checking for the open element stack empty.
+     **/
+
+    public void end()
+    {
+        if (_openTag)
+            closeTag();
+
+        _writer.print("</");
+        _writer.print(pop());
+        _writer.print('>');
+    }
+
+    /**
+     * Ends the most recently started element with the given name. This will
+     * also end any other intermediate elements. This is very useful for easily
+     * ending a table or even an entire page.
+     *
+     * <p>TBD: Error check if the name matches nothing on the open tag stack.
+     **/
+
+    public void end(String name)
+    {
+        if (_openTag)
+            closeTag();
+
+        while (true)
+        {
+            String tagName = pop();
+
+            _writer.print("</");
+            _writer.print(tagName);
+            _writer.print('>');
+
+            if (tagName.equals(name))
+                break;
+        }
+    }
+
+    /**
+     * Forwards <code>flush()</code> to this <code>AbstractMarkupWriter</code>'s 
+     * <code>PrintWriter</code>.
+     *
+     **/
+
+    public void flush()
+    {
+        _writer.flush();
+    }
+
+    /**
+     *  Removes the top element from the active element stack and returns it.
+     *
+     **/
+
+    protected final String pop()
+    {
+        String result = (String) _activeElementStack.pop();
+        _depth--;
+
+        return result;
+    }
+
+    /**
+     *
+     * The primary <code>print()</code> method, used by most other methods.
+     *
+     * <p>Prints the character array, first closing any open tag. Problematic characters
+     * ('&lt;', '&gt;' and '&amp;') are converted to their
+     * GTML entities.
+     *
+     * <p>All 'unsafe' characters are properly converted to either a named
+     * or numeric GTML entity.  This can be somewhat expensive, so use
+     * {@link #printRaw(char[], int, int)} if the data to print is guarenteed
+     * to be safe.
+     *
+     * <p>Does <em>nothing</em> if <code>data</code> is null.
+     *
+     * <p>Closes any open tag.
+     *
+     **/
+
+    public void print(char[] data, int offset, int length)
+    {
+        if (data == null)
+            return;
+
+        if (_openTag)
+            closeTag();
+
+        safePrint(data, offset, length, false);
+    }
+
+    /**
+     * Prints a single character. If the character is not a 'safe' character,
+     * such as '&lt;', then it's GTML entity (named or numeric) is printed instead.
+     *
+     * <p>Closes any open tag.
+     *
+     **/
+
+    public void print(char value)
+    {
+        if (_openTag)
+            closeTag();
+
+        if (value < _safe.length && _safe[value])
+        {
+            _writer.print(value);
+            return;
+        }
+
+        String entity = null;
+
+        if (value < _entities.length)
+            entity = _entities[value];
+
+        if (entity != null)
+        {
+            _writer.print(entity);
+            return;
+        }
+
+        // Not a well-known entity.  Print it's numeric equivalent.  Note:  this omits
+        // the leading '0', but most browsers (IE 5.0) don't seem to mind.  Is this a bug?
+
+        _writer.print("&#" + (int) value + ";");
+    }
+
+    /**
+     * Prints an integer.
+     *
+     * <p>Closes any open tag.
+     *
+     **/
+
+    public void print(int value)
+    {
+        if (_openTag)
+            closeTag();
+
+        _writer.print(value);
+    }
+
+    /**
+     * Invokes {@link #print(char[], int, int)} to print the string.  Use
+     * {@link #printRaw(String)} if the character data is known to be safe.
+     *
+     * <p>Does <em>nothing</em> if <code>value</code> is null.
+     *
+     * <p>Closes any open tag.
+     *
+     * @see #print(char[], int, int)
+     *
+     **/
+
+    public void print(String value)
+    {
+        if (value == null)
+            return;
+
+        int length = value.length();
+
+        if (_buffer == null || _buffer.length < length)
+            _buffer = new char[length];
+
+        value.getChars(0, length, _buffer, 0);
+
+        print(_buffer, 0, length);
+    }
+
+    /**
+     * Closes the open tag (if any), then prints a line seperator to the output stream.
+     *
+     **/
+
+    public void println()
+    {
+        if (_openTag)
+            closeTag();
+
+        _writer.println();
+    }
+
+    /**
+     * Prints and portion of an output buffer to the stream.
+     * No escaping of invalid GTML elements is done, which
+     * makes this more effecient than <code>print()</code>. 
+     * Does <em>nothing</em> if <code>buffer</code>
+     * is null.
+     *
+     * <p>Closes any open tag.
+     *
+     **/
+
+    public void printRaw(char[] buffer, int offset, int length)
+    {
+        if (buffer == null)
+            return;
+
+        if (_openTag)
+            closeTag();
+
+        _writer.write(buffer, offset, length);
+    }
+
+    /**
+     * Prints output to the stream. No escaping of invalid GTML elements is done, which
+     * makes this more effecient than <code>print()</code>. Does <em>nothing</em> 
+     * if <code>value</code>
+     * is null.
+     *
+     * <p>Closes any open tag.
+     *
+     **/
+
+    public void printRaw(String value)
+    {
+        if (value == null)
+            return;
+
+        if (_openTag)
+            closeTag();
+
+        _writer.print(value);
+    }
+
+    /**
+     *  Adds an element to the active element stack.
+     *
+     **/
+
+    protected final void push(String name)
+    {
+        if (_activeElementStack == null)
+            _activeElementStack = new Stack();
+
+        _activeElementStack.push(name);
+
+        _depth++;
+    }
+
+    /**
+     * Internal support for safe printing.  Ensures that all characters emitted
+     * are safe: either valid GTML characters or GTML entities (named or numeric).
+     **/
+
+    private void safePrint(char[] data, int offset, int length, boolean isAttribute)
+    {
+        int safelength = 0;
+        int start = offset;
+
+        for (int i = 0; i < length; i++)
+        {
+            char ch = data[offset + i];
+
+            // Ignore safe characters.  In an attribute, quotes
+            // are not ok and are escaped.
+
+            boolean isSafe = (ch < _safe.length && _safe[ch]);
+
+            if (isAttribute && ch == '"')
+                isSafe = false;
+
+            if (isSafe)
+            {
+                safelength++;
+                continue;
+            }
+
+            // Write the safe stuff.
+
+            if (safelength > 0)
+                _writer.write(data, start, safelength);
+
+            String entity = null;
+
+            // Look for a known entity.
+
+            if (ch < _entities.length)
+                entity = _entities[ch];
+
+            // Failing that, emit a numeric entity.
+
+            if (entity == null)
+                entity = "&#" + (int) ch + ";";
+
+            _writer.print(entity);
+
+            start = offset + i + 1;
+            safelength = 0;
+        }
+
+        if (safelength > 0)
+            _writer.write(data, start, safelength);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/AbstractPage.java b/tapestry-framework/src/org/apache/tapestry/AbstractPage.java
new file mode 100644
index 0000000..67ad269
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/AbstractPage.java
@@ -0,0 +1,576 @@
+//  Copyright 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.tapestry;
+
+import java.io.OutputStream;
+import java.util.EventListener;
+import java.util.Locale;
+
+import javax.swing.event.EventListenerList;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.event.ChangeObserver;
+import org.apache.tapestry.event.PageDetachListener;
+import org.apache.tapestry.event.PageEvent;
+import org.apache.tapestry.event.PageRenderListener;
+import org.apache.tapestry.event.PageValidateListener;
+import org.apache.tapestry.util.StringSplitter;
+
+/**
+ *  Abstract base class implementing the {@link IPage} interface.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship, David Solis
+ *  @since 0.2.9
+ * 
+ **/
+
+public abstract class AbstractPage extends BaseComponent implements IPage
+{
+    private static final Log LOG = LogFactory.getLog(AbstractPage.class);
+
+    /**
+     *  Object to be notified when a observered property changes.  Observered
+     *  properties are the ones that will be persisted between request cycles.
+     *  Unobserved properties are reconstructed.
+     *
+     **/
+
+    private ChangeObserver _changeObserver;
+
+    /**
+     *  The {@link IEngine} the page is currently attached to.
+     *
+     **/
+
+    private IEngine _engine;
+
+    /**
+     *  The visit object, if any, for the application.  Set inside
+     *  {@link #attach(IEngine)} and cleared
+     *  by {@link #detach()}.
+     *
+     **/
+
+    private Object _visit;
+
+    /**
+     *  The qualified name of the page, which may be prefixed by the
+     *  namespace.
+     * 
+     *  @since 2.3
+     * 
+     **/
+
+    private String _pageName;
+
+    /**
+     *  Set when the page is attached to the engine.
+     *
+     **/
+
+    private IRequestCycle _requestCycle;
+
+    /**
+     *  The locale of the page, initially determined from the {@link IEngine engine}.
+     *
+     **/
+
+    private Locale _locale;
+
+    /**
+     *  A list of listeners for the page.
+     *  @see PageRenderListener
+     *  @see PageDetachListener
+     *
+     *  @since 1.0.5
+     **/
+
+    private EventListenerList _listenerList;
+    
+    
+    /**
+     *  The output encoding to be used when rendering this page.
+     *  This value is cached from the engine.
+     *
+     *  @since 3.0
+     **/
+    private String _outputEncoding;
+
+    /**
+     *  Standard constructor; invokes {@link #initialize()}
+     *  to configure initial values for properties
+     *  of the page.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public AbstractPage()
+    {
+        initialize();
+    }
+
+    /**
+     *  Implemented in subclasses to provide a particular kind of
+     *  response writer (and therefore, a particular kind of
+     *  content).
+     *
+     **/
+
+    abstract public IMarkupWriter getResponseWriter(OutputStream out);
+
+    /**
+     *  Prepares the page to be returned to the pool.
+     *  <ul>
+     *  <li>Clears the changeObserved property
+     *	<li>Invokes {@link PageDetachListener#pageDetached(PageEvent)} on all listeners
+     *  <li>Invokes {@link #initialize()} to clear/reset any properties	
+     * <li>Clears the engine, visit and requestCycle properties
+     *	</ul>
+     *
+     *  <p>Subclasses may override this method, but must invoke this
+     *  implementation (usually, last).
+     *
+     **/
+
+    public void detach()
+    {
+    	Tapestry.addMethodInvocation(Tapestry.ABSTRACTPAGE_DETACH_METHOD_ID);
+    	
+        // Do this first,so that any changes to persistent properties do not
+        // cause errors.
+
+        _changeObserver = null;
+
+        firePageDetached();
+
+        initialize();
+
+        _engine = null;
+        _visit = null;
+        _requestCycle = null;
+    }
+
+    /**
+     *  Method invoked from the constructor, and from
+     *  {@link #detach()} to (re-)initialize properties
+     *  of the page.  This is most useful when
+     *  properties have non-null initial values.
+     * 
+     *  <p>Subclasses may override this implementation
+     *  (which is empty).
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    protected void initialize()
+    {
+        // Does nothing.
+    }
+
+    public IEngine getEngine()
+    {
+        return _engine;
+    }
+
+    public ChangeObserver getChangeObserver()
+    {
+        return _changeObserver;
+    }
+
+    /**
+     *  Returns the name of the page.
+     *
+     **/
+
+    public String getExtendedId()
+    {
+        return _pageName;
+    }
+
+    /**
+     *  Pages always return null for idPath.
+     *
+     **/
+
+    public String getIdPath()
+    {
+        return null;
+    }
+
+    /**
+     *  Returns the locale for the page, which may be null if the
+     *  locale is not known (null corresponds to the "default locale").
+     *
+     **/
+
+    public Locale getLocale()
+    {
+        return _locale;
+    }
+
+    public void setLocale(Locale value)
+    {
+        if (_locale != null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("AbstractPage.attempt-to-change-locale"));
+
+        _locale = value;
+    }
+
+    public IComponent getNestedComponent(String path)
+    {
+        StringSplitter splitter;
+        IComponent current;
+        String[] elements;
+        int i;
+
+        if (path == null)
+            return this;
+
+        splitter = new StringSplitter('.');
+        current = this;
+
+        elements = splitter.splitToArray(path);
+        for (i = 0; i < elements.length; i++)
+        {
+            current = current.getComponent(elements[i]);
+        }
+
+        return current;
+
+    }
+
+    /**
+     *  Called by the {@link IEngine engine} to attach the page
+     *  to itself.  Does
+     *  <em>not</em> change the locale, but since a page is selected
+     *  from the {@link org.apache.tapestry.engine.IPageSource} pool based on its
+     *  locale matching the engine's locale, they should match
+     *  anyway.
+     *
+     **/
+
+    public void attach(IEngine value)
+    {
+        if (_engine != null)
+            LOG.error(this +" attach(" + value + "), but engine = " + _engine);
+
+        _engine = value;
+    }
+
+    /**
+     *
+     * <ul>
+     *  <li>Invokes {@link PageRenderListener#pageBeginRender(PageEvent)}
+     *  <li>Invokes {@link #beginResponse(IMarkupWriter, IRequestCycle)}
+     *  <li>Invokes {@link IRequestCycle#commitPageChanges()} (if not rewinding)
+     *  <li>Invokes {@link #render(IMarkupWriter, IRequestCycle)}
+     *  <li>Invokes {@link PageRenderListener#pageEndRender(PageEvent)} (this occurs
+     *  even if a previous step throws an exception)
+     *
+     **/
+
+    public void renderPage(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        try
+        {
+            firePageBeginRender();
+
+            beginResponse(writer, cycle);
+
+            if (!cycle.isRewinding())
+                cycle.commitPageChanges();
+
+            render(writer, cycle);
+        }
+        finally
+        {
+            firePageEndRender();
+        }
+    }
+
+    public void setChangeObserver(ChangeObserver value)
+    {
+        _changeObserver = value;
+    }
+
+    /** @since 3.0 **/
+
+    public void setPageName(String pageName)
+    {
+        if (_pageName != null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("AbstractPage.attempt-to-change-name"));
+
+        _pageName = pageName;
+    }
+
+    /**
+     *  By default, pages are not protected and this method does nothing.
+     *
+     **/
+
+    public void validate(IRequestCycle cycle)
+    {
+        Tapestry.addMethodInvocation(Tapestry.ABSTRACTPAGE_VALIDATE_METHOD_ID);
+
+        firePageValidate();
+    }
+
+    /**
+     *  Does nothing, subclasses may override as needed.
+     *
+     * @deprecated To be removed in 3.1.  Implement 
+     * {@link PageRenderListener} instead.
+     *
+     **/
+
+    public void beginResponse(IMarkupWriter writer, IRequestCycle cycle)
+    {
+    }
+
+    public IRequestCycle getRequestCycle()
+    {
+        return _requestCycle;
+    }
+
+    public void setRequestCycle(IRequestCycle value)
+    {
+        _requestCycle = value;
+    }
+
+    /**
+     *  Returns the visit object obtained from the engine via
+     *  {@link IEngine#getVisit(IRequestCycle)}.
+     *
+     **/
+
+    public Object getVisit()
+    {
+        if (_visit == null)
+            _visit = _engine.getVisit(_requestCycle);
+
+        return _visit;
+    }
+
+    /**
+     *  Convienience methods, simply invokes
+     *  {@link IEngine#getGlobal()}.
+     * 
+     *  @since 2.3
+     * 
+     **/
+
+    public Object getGlobal()
+    {
+        return _engine.getGlobal();
+    }
+
+    public void addPageDetachListener(PageDetachListener listener)
+    {
+        addListener(PageDetachListener.class, listener);
+    }
+
+    private void addListener(Class listenerClass, EventListener listener)
+    {
+        if (_listenerList == null)
+            _listenerList = new EventListenerList();
+
+        _listenerList.add(listenerClass, listener);
+    }
+
+    /**
+     *  @since 2.1-beta-2
+     * 
+     **/
+
+    private void removeListener(Class listenerClass, EventListener listener)
+    {
+        if (_listenerList != null)
+            _listenerList.remove(listenerClass, listener);
+    }
+
+    public void addPageRenderListener(PageRenderListener listener)
+    {
+        addListener(PageRenderListener.class, listener);
+    }
+
+    /**
+     *  @since 1.0.5
+     *
+     **/
+
+    protected void firePageDetached()
+    {
+        if (_listenerList == null)
+            return;
+
+        PageEvent event = null;
+        Object[] listeners = _listenerList.getListenerList();
+
+        for (int i = 0; i < listeners.length; i += 2)
+        {
+            if (listeners[i] == PageDetachListener.class)
+            {
+                PageDetachListener l = (PageDetachListener) listeners[i + 1];
+
+                if (event == null)
+                    event = new PageEvent(this, _requestCycle);
+
+                l.pageDetached(event);
+            }
+        }
+    }
+
+    /**
+     *  @since 1.0.5
+     *
+     **/
+
+    protected void firePageBeginRender()
+    {
+        if (_listenerList == null)
+            return;
+
+        PageEvent event = null;
+        Object[] listeners = _listenerList.getListenerList();
+
+        for (int i = 0; i < listeners.length; i += 2)
+        {
+            if (listeners[i] == PageRenderListener.class)
+            {
+                PageRenderListener l = (PageRenderListener) listeners[i + 1];
+
+                if (event == null)
+                    event = new PageEvent(this, _requestCycle);
+
+                l.pageBeginRender(event);
+            }
+        }
+    }
+
+    /**
+     *  @since 1.0.5
+     *
+     **/
+
+    protected void firePageEndRender()
+    {
+        if (_listenerList == null)
+            return;
+
+        PageEvent event = null;
+        Object[] listeners = _listenerList.getListenerList();
+
+        for (int i = 0; i < listeners.length; i += 2)
+        {
+            if (listeners[i] == PageRenderListener.class)
+            {
+                PageRenderListener l = (PageRenderListener) listeners[i + 1];
+
+                if (event == null)
+                    event = new PageEvent(this, _requestCycle);
+
+                l.pageEndRender(event);
+            }
+        }
+    }
+
+    /**
+     *  @since 2.1-beta-2
+     * 
+     **/
+
+    public void removePageDetachListener(PageDetachListener listener)
+    {
+        removeListener(PageDetachListener.class, listener);
+    }
+
+    public void removePageRenderListener(PageRenderListener listener)
+    {
+        removeListener(PageRenderListener.class, listener);
+    }
+
+    /** @since 2.2 **/
+
+    public void beginPageRender()
+    {
+        firePageBeginRender();
+    }
+
+    /** @since 2.2 **/
+
+    public void endPageRender()
+    {
+        firePageEndRender();
+    }
+
+    /** @since 3.0 **/
+
+    public String getPageName()
+    {
+        return _pageName;
+    }
+
+    public void addPageValidateListener(PageValidateListener listener)
+    {
+        addListener(PageValidateListener.class, listener);
+    }
+
+    public void removePageValidateListener(PageValidateListener listener)
+    {
+        removeListener(PageValidateListener.class, listener);
+    }
+
+    protected void firePageValidate()
+    {
+        if (_listenerList == null)
+            return;
+
+        PageEvent event = null;
+        Object[] listeners = _listenerList.getListenerList();
+
+        for (int i = 0; i < listeners.length; i += 2)
+        {
+            if (listeners[i] == PageValidateListener.class)
+            {
+                PageValidateListener l = (PageValidateListener) listeners[i + 1];
+
+                if (event == null)
+                    event = new PageEvent(this, _requestCycle);
+
+                l.pageValidate(event);
+            }
+        }
+    }
+
+    /**
+     *  Returns the output encoding to be used when rendering this page.
+     *  This value is usually cached from the Engine.
+     * 
+     *  @since 3.0
+     **/
+    protected String getOutputEncoding()
+    {
+        if (_outputEncoding == null)
+            _outputEncoding = getEngine().getOutputEncoding();
+        
+        return _outputEncoding;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/ApplicationRuntimeException.java b/tapestry-framework/src/org/apache/tapestry/ApplicationRuntimeException.java
new file mode 100644
index 0000000..dfcf792
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/ApplicationRuntimeException.java
@@ -0,0 +1,83 @@
+//  Copyright 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.tapestry;
+
+/**
+ *  General wrapper for any exception (normal or runtime) that may occur during
+ *  runtime processing for the application.  This exception is used
+ *  when the intent is to communicate a low-level failure to the user or
+ *  developer; it is not expected to be caught.  The {@link #getCause() rootCause}
+ *  property is a <em>nested</em> exception (Tapestry supported this concept
+ *  long before the JDK did).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ **/
+
+public class ApplicationRuntimeException extends RuntimeException implements ILocatable
+{
+    private Throwable _rootCause;
+    private transient ILocation _location;
+    private transient Object _component;
+
+    public ApplicationRuntimeException(Throwable rootCause)
+    {
+        this(rootCause.getMessage(), rootCause);
+    }
+
+    public ApplicationRuntimeException(String message)
+    {
+        this(message, null, null, null);
+    }
+
+    public ApplicationRuntimeException(String message, Throwable rootCause)
+    {
+        this(message, null, null, rootCause);
+    }
+
+    public ApplicationRuntimeException(
+        String message,
+        Object component,
+        ILocation location,
+        Throwable rootCause)
+    {
+        super(message);
+
+        _rootCause = rootCause;
+        _component = component;
+
+        _location = Tapestry.findLocation(new Object[] { location, rootCause, component });
+    }
+
+    public ApplicationRuntimeException(String message, ILocation location, Throwable rootCause)
+    {
+        this(message, null, location, rootCause);
+    }
+
+    public Throwable getCause()
+    {
+        return _rootCause;
+    }
+
+    public ILocation getLocation()
+    {
+        return _location;
+    }
+
+    public Object getComponent()
+    {
+        return _component;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/ApplicationServlet.java b/tapestry-framework/src/org/apache/tapestry/ApplicationServlet.java
new file mode 100644
index 0000000..80ce4a0
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/ApplicationServlet.java
@@ -0,0 +1,807 @@
+//  Copyright 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.tapestry;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Locale;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.engine.BaseEngine;
+import org.apache.tapestry.engine.IPropertySource;
+import org.apache.tapestry.parse.SpecificationParser;
+import org.apache.tapestry.request.RequestContext;
+import org.apache.tapestry.resource.ClasspathResourceLocation;
+import org.apache.tapestry.resource.ContextResourceLocation;
+import org.apache.tapestry.spec.ApplicationSpecification;
+import org.apache.tapestry.spec.IApplicationSpecification;
+import org.apache.tapestry.util.DefaultResourceResolver;
+import org.apache.tapestry.util.DelegatingPropertySource;
+import org.apache.tapestry.util.JanitorThread;
+import org.apache.tapestry.util.ServletContextPropertySource;
+import org.apache.tapestry.util.ServletPropertySource;
+import org.apache.tapestry.util.SystemPropertiesPropertySource;
+import org.apache.tapestry.util.exception.ExceptionAnalyzer;
+import org.apache.tapestry.util.pool.Pool;
+import org.apache.tapestry.util.xml.DocumentParseException;
+
+/**
+ *  Links a servlet container with a Tapestry application.  The servlet has some
+ *  responsibilities related to bootstrapping the application (in terms of
+ *  logging, reading the {@link ApplicationSpecification specification}, etc.).
+ *  It is also responsible for creating or locating the {@link IEngine} and delegating
+ *  incoming requests to it.
+ * 
+ *  <p>The servlet init parameter
+ *  <code>org.apache.tapestry.specification-path</code>
+ *  should be set to the complete resource path (within the classpath)
+ *  to the application specification, i.e.,
+ *  <code>/com/foo/bar/MyApp.application</code>. 
+ *
+ *  <p>In some servlet containers (notably
+ *  <a href="www.bea.com"/>WebLogic</a>)
+ *  it is necessary to invoke {@link HttpSession#setAttribute(String,Object)}
+ *  in order to force a persistent value to be replicated to the other
+ *  servers in the cluster.  Tapestry applications usually only have a single
+ *  persistent value, the {@link IEngine engine}.  For persistence to
+ *  work in such an environment, the
+ *  JVM system property <code>org.apache.tapestry.store-engine</code>
+ *  must be set to <code>true</code>.  This will force the application
+ *  servlet to restore the engine into the {@link HttpSession} at the
+ *  end of each request cycle.
+ * 
+ *  <p>As of release 1.0.1, it is no longer necessary for a {@link HttpSession}
+ *  to be created on the first request cycle.  Instead, the HttpSession is created
+ *  as needed by the {@link IEngine} ... that is, when a visit object is created,
+ *  or when persistent page state is required.  Otherwise, for sessionless requests,
+ *  an {@link IEngine} from a {@link Pool} is used.  Additional work must be done
+ *  so that the {@link IEngine} can change locale <em>without</em> forcing 
+ *  the creation of a session; this involves the servlet and the engine storing
+ *  locale information in a {@link Cookie}.
+ * 
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ * 
+ **/
+
+public class ApplicationServlet extends HttpServlet
+{
+    private static final Log LOG = LogFactory.getLog(ApplicationServlet.class);
+
+    /** @since 2.3 **/
+
+    private static final String APP_SPEC_PATH_PARAM =
+        "org.apache.tapestry.application-specification";
+
+    /**
+     *  Name of the cookie written to the client web browser to
+     *  identify the locale.
+     *
+     **/
+
+    private static final String LOCALE_COOKIE_NAME = "org.apache.tapestry.locale";
+
+    /**
+     *  A {@link Pool} used to store {@link IEngine engine}s that are not currently
+     *  in use.  The key is on {@link Locale}.
+     *
+     **/
+
+    private Pool _enginePool = new Pool();
+
+    /**
+     *  The application specification, which is read once and kept in memory
+     *  thereafter.
+     *
+     **/
+
+    private IApplicationSpecification _specification;
+
+    /**
+     * The name under which the {@link IEngine engine} is stored within the
+     * {@link HttpSession}.
+     *
+     **/
+
+    private String _attributeName;
+
+    /**
+     *  The resolved class name used to instantiate the engine.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    private String _engineClassName;
+
+    /**
+     *  Used to search for configuration properties.
+     * 
+     *  
+     *  @since 3.0
+     * 
+     **/
+
+    private IPropertySource _propertySource;
+
+    /**
+     *  Invokes {@link #doService(HttpServletRequest, HttpServletResponse)}.
+     *
+     *  @since 1.0.6
+     *
+     **/
+
+    public void doGet(HttpServletRequest request, HttpServletResponse response)
+        throws IOException, ServletException
+    {
+        doService(request, response);
+    }
+
+    /**
+     *  @since 2.3
+     * 
+     **/
+
+    private IResourceResolver _resolver;
+
+    /**
+     * Handles the GET and POST requests. Performs the following:
+     * <ul>
+     * <li>Construct a {@link RequestContext}
+     * <li>Invoke {@link #getEngine(RequestContext)} to get or create the {@link IEngine}
+     * <li>Invoke {@link IEngine#service(RequestContext)} on the application
+     * </ul>
+     **/
+
+    protected void doService(HttpServletRequest request, HttpServletResponse response)
+        throws IOException, ServletException
+    {
+        RequestContext context = null;
+
+        try
+        {
+
+            // Create a context from the various bits and pieces.
+
+            context = createRequestContext(request, response);
+
+            // The subclass provides the engine.
+
+            IEngine engine = getEngine(context);
+
+            if (engine == null)
+                throw new ServletException(
+                    Tapestry.getMessage("ApplicationServlet.could-not-locate-engine"));
+
+            boolean dirty = engine.service(context);
+
+            HttpSession session = context.getSession();
+
+            // When there's an active session, we *may* store it into
+            // the HttpSession and we *will not* store the engine
+            // back into the engine pool.
+
+            if (session != null)
+            {
+                // If the service may have changed the engine and the
+                // special storeEngine flag is on, then re-save the engine
+                // into the session.  Otherwise, we only save the engine
+                // into the session when the session is first created (is new).
+
+                try
+                {
+
+                    boolean forceStore =
+                        engine.isStateful() && (session.getAttribute(_attributeName) == null);
+
+                    if (forceStore || dirty)
+                    {
+                        if (LOG.isDebugEnabled())
+                            LOG.debug("Storing " + engine + " into session as " + _attributeName);
+
+                        session.setAttribute(_attributeName, engine);
+                    }
+                }
+                catch (IllegalStateException ex)
+                {
+                    // Ignore because the session been's invalidated.
+                    // Allow the engine (which has state particular to the client)
+                    // to be reclaimed by the garbage collector.
+
+                    if (LOG.isDebugEnabled())
+                        LOG.debug("Session invalidated.");
+                }
+
+                // The engine is stateful and stored in a session.  Even if it started
+                // the request cycle in the pool, it doesn't go back.
+
+                return;
+            }
+
+            if (engine.isStateful())
+            {
+                LOG.error(
+                    Tapestry.format(
+                        "ApplicationServlet.engine-stateful-without-session",
+                        engine));
+                return;
+            }
+
+            // No session; the engine contains no state particular to
+            // the client (except for locale).  Don't throw it away,
+            // instead save it in a pool for later reuse (by this, or another
+            // client in the same locale).
+
+            if (LOG.isDebugEnabled())
+                LOG.debug("Returning " + engine + " to pool.");
+
+            _enginePool.store(engine.getLocale(), engine);
+
+        }
+        catch (ServletException ex)
+        {
+            log("ServletException", ex);
+
+            show(ex);
+
+            // Rethrow it.
+
+            throw ex;
+        }
+        catch (IOException ex)
+        {
+            log("IOException", ex);
+
+            show(ex);
+
+            // Rethrow it.
+
+            throw ex;
+        }
+        finally
+        {
+            if (context != null)
+                context.cleanup();
+        }
+
+    }
+
+    /**
+     *  Invoked by {@link #doService(HttpServletRequest, HttpServletResponse)} to create
+     *  the {@link RequestContext} for this request cycle.  Some applications may need to
+     *  replace the default RequestContext with a subclass for particular behavior.
+     * 
+     *  @since 2.3
+     * 
+     **/
+
+    protected RequestContext createRequestContext(
+        HttpServletRequest request,
+        HttpServletResponse response)
+        throws IOException
+    {
+        return new RequestContext(this, request, response);
+    }
+
+    protected void show(Exception ex)
+    {
+        System.err.println("\n\n**********************************************************\n\n");
+
+        new ExceptionAnalyzer().reportException(ex, System.err);
+
+        System.err.println("\n**********************************************************\n");
+
+    }
+
+    /**
+     *  Invokes {@link #doService(HttpServletRequest, HttpServletResponse)}.
+     *
+     *
+     **/
+
+    public void doPost(HttpServletRequest request, HttpServletResponse response)
+        throws IOException, ServletException
+    {
+        doService(request, response);
+    }
+
+    /**
+     *  Returns the application specification, which is read
+     *  by the {@link #init(ServletConfig)} method.
+     *
+     **/
+
+    public IApplicationSpecification getApplicationSpecification()
+    {
+        return _specification;
+    }
+
+    /**
+     *  Retrieves the {@link IEngine engine} that will process this
+     *  request.  This comes from one of the following places:
+     *  <ul>
+     *  <li>The {@link HttpSession}, if the there is one.
+     *  <li>From the pool of available engines
+     *  <li>Freshly created
+     *  </ul>
+     *
+     **/
+
+    protected IEngine getEngine(RequestContext context) throws ServletException
+    {
+        IEngine engine = null;
+        HttpSession session = context.getSession();
+
+        // If there's a session, then find the engine within it.
+
+        if (session != null)
+        {
+            engine = (IEngine) session.getAttribute(_attributeName);
+            if (engine != null)
+            {
+                if (LOG.isDebugEnabled())
+                    LOG.debug("Retrieved " + engine + " from session " + session.getId() + ".");
+
+                return engine;
+            }
+
+            if (LOG.isDebugEnabled())
+                LOG.debug("Session exists, but doesn't contain an engine.");
+        }
+
+        Locale locale = getLocaleFromRequest(context);
+
+        engine = (IEngine) _enginePool.retrieve(locale);
+
+        if (engine == null)
+        {
+            engine = createEngine(context);
+            engine.setLocale(locale);
+        }
+        else
+        {
+            if (LOG.isDebugEnabled())
+                LOG.debug("Using pooled engine " + engine + " (from locale " + locale + ").");
+        }
+
+        return engine;
+    }
+
+    /**
+     *  Determines the {@link Locale} for the incoming request.
+     *  This is determined from the locale cookie or, if not set,
+     *  from the request itself.  This may return null
+     *  if no locale is determined.
+     *
+     **/
+
+    protected Locale getLocaleFromRequest(RequestContext context) throws ServletException
+    {
+        Cookie cookie = context.getCookie(LOCALE_COOKIE_NAME);
+
+        if (cookie != null)
+            return Tapestry.getLocale(cookie.getValue());
+
+        return context.getRequest().getLocale();
+    }
+
+    /**
+     *  Reads the application specification when the servlet is
+     *  first initialized.  All {@link IEngine engine instances}
+     *  will have access to the specification via the servlet.
+     * 
+     *  @see #getApplicationSpecification()
+     *  @see #constructApplicationSpecification()
+     *  @see #createResourceResolver()
+     *
+     **/
+
+    public void init(ServletConfig config) throws ServletException
+    {
+        super.init(config);
+
+        _resolver = createResourceResolver();
+
+        _specification = constructApplicationSpecification();
+
+        _attributeName = "org.apache.tapestry.engine:" + config.getServletName();
+    }
+
+    /**
+     *  Invoked from {@link #init(ServletConfig)} to create a resource resolver
+     *  for the servlet (which will utlimately be shared and used through the
+     *  application).
+     * 
+     *  <p>This implementation constructs a {@link DefaultResourceResolver}, subclasses
+     *  may provide a different implementation.
+     * 
+     *  @see #getResourceResolver()
+     *  @since 2.3
+     * 
+     **/
+
+    protected IResourceResolver createResourceResolver() throws ServletException
+    {
+        return new DefaultResourceResolver();
+    }
+
+    /**
+     *  Invoked from {@link #init(ServletConfig)} to read and construct
+     *  the {@link ApplicationSpecification} for this servlet.
+     *  Invokes {@link #getApplicationSpecificationPath()}, opens
+     *  the resource as a stream, then invokes
+     *  {@link #parseApplicationSpecification(IResourceLocation)}.
+     * 
+     *  <p>
+     *  This method exists to be overriden in
+     *  applications where the application specification cannot be
+     *  loaded from the classpath.  Alternately, a subclass
+     *  could override this method, invoke this implementation,
+     *  and then add additional data to it (for example, an application
+     *  where some of the pages are defined in an external source
+     *  such as a database).
+     *  
+     *  @since 2.2
+     * 
+     **/
+
+    protected IApplicationSpecification constructApplicationSpecification() throws ServletException
+    {
+        IResourceLocation specLocation = getApplicationSpecificationLocation();
+
+        if (specLocation == null)
+        {
+            if (LOG.isDebugEnabled())
+                LOG.debug(Tapestry.getMessage("ApplicationServlet.no-application-specification"));
+
+            return constructStandinSpecification();
+        }
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Loading application specification from " + specLocation);
+
+        return parseApplicationSpecification(specLocation);
+    }
+
+    /**
+     *  Gets the location of the application specification, if there is one.
+     *  
+     *  <ul>
+     *  <li>Invokes {@link #getApplicationSpecificationPath()} to get the
+     *  location of the application specification on the classpath.
+     *  <li>If that return null, search for the application specification:
+     *  <ul>
+     *  <li><i>name</i>.application in /WEB-INF/<i>name</i>/
+     *  <li><i>name</i>.application in /WEB-INF/
+     *  </ul>
+     *  </ul>
+     * 
+     *  <p>Returns the location of the application specification, or null
+     *  if not found.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    protected IResourceLocation getApplicationSpecificationLocation() throws ServletException
+    {
+        String path = getApplicationSpecificationPath();
+
+        if (path != null)
+            return new ClasspathResourceLocation(_resolver, path);
+
+        ServletContext context = getServletContext();
+        String servletName = getServletName();
+        String expectedName = servletName + ".application";
+
+        IResourceLocation webInfLocation = new ContextResourceLocation(context, "/WEB-INF/");
+        IResourceLocation webInfAppLocation = webInfLocation.getRelativeLocation(servletName + "/");
+
+        IResourceLocation result = check(webInfAppLocation, expectedName);
+        if (result != null)
+            return result;
+
+        return check(webInfLocation, expectedName);
+    }
+
+    /**
+     *  Checks for the application specification relative to the specified
+     *  location.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    private IResourceLocation check(IResourceLocation location, String name)
+    {
+        IResourceLocation result = location.getRelativeLocation(name);
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Checking for existence of " + result);
+
+        if (result.getResourceURL() != null)
+        {
+            LOG.debug("Found.");
+            return result;
+        }
+
+        return null;
+    }
+
+    /**
+     *  Invoked from {@link #constructApplicationSpecification()} when
+     *  the application doesn't have an explicit specification.  A
+     *  simple specification is constructed and returned.  This is useful
+     *  for minimal applications and prototypes.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    protected IApplicationSpecification constructStandinSpecification()
+    {
+        ApplicationSpecification result = new ApplicationSpecification();
+
+        IResourceLocation virtualLocation =
+            new ContextResourceLocation(getServletContext(), "/WEB-INF/");
+
+        result.setSpecificationLocation(virtualLocation);
+
+        result.setName(getServletName());
+        result.setResourceResolver(_resolver);
+
+        return result;
+    }
+
+    /**
+     *  Invoked from {@link #constructApplicationSpecification()} to
+     *  actually parse the stream (with content provided from the path)
+     *  and convert it into an {@link ApplicationSpecification}.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    protected IApplicationSpecification parseApplicationSpecification(IResourceLocation location)
+        throws ServletException
+    {
+        try
+        {
+            SpecificationParser parser = new SpecificationParser(_resolver);
+
+            return parser.parseApplicationSpecification(location);
+        }
+        catch (DocumentParseException ex)
+        {
+            show(ex);
+
+            throw new ServletException(
+                Tapestry.format("ApplicationServlet.could-not-parse-spec", location),
+                ex);
+        }
+    }
+
+    /**
+     *  Closes the stream, ignoring any exceptions.
+     * 
+     **/
+
+    protected void close(InputStream stream)
+    {
+        try
+        {
+            if (stream != null)
+                stream.close();
+        }
+        catch (IOException ex)
+        {
+            // Ignore it.
+        }
+    }
+
+    /**
+     *  Reads the servlet init parameter
+     *  <code>org.apache.tapestry.application-specification</code>, which
+     *  is the location, on the classpath, of the application specification.
+     *
+     *  <p>
+     *  If the parameter is not set, this method returns null, and a search
+     *  for the application specification within the servlet context
+     *  will begin.
+     * 
+     *  @see #getApplicationSpecificationLocation()
+     * 
+     **/
+
+    protected String getApplicationSpecificationPath() throws ServletException
+    {
+        return getInitParameter(APP_SPEC_PATH_PARAM);
+    }
+
+    /**
+     *  Invoked by {@link #getEngine(RequestContext)} to create
+     *  the {@link IEngine} instance specific to the
+     *  application, if not already in the
+     *  {@link HttpSession}.
+     *
+     *  <p>The {@link IEngine} instance returned is stored into the
+     *  {@link HttpSession}.
+     *
+     *  @see #getEngineClassName()    
+     *
+     **/
+
+    protected IEngine createEngine(RequestContext context) throws ServletException
+    {
+        try
+        {
+            String className = getEngineClassName();
+
+            if (LOG.isDebugEnabled())
+                LOG.debug("Creating engine from class " + className);
+
+            Class engineClass = getResourceResolver().findClass(className);
+
+            IEngine result = (IEngine) engineClass.newInstance();
+
+            if (LOG.isDebugEnabled())
+                LOG.debug("Created engine " + result);
+
+            return result;
+        }
+        catch (Exception ex)
+        {
+            throw new ServletException(ex);
+        }
+    }
+
+    /**
+     * 
+     *  Returns the name of the class to use when instantiating
+     *  an engine instance for this application.  
+     *  If the application specification
+     *  provides a value, this is returned.  Otherwise, a search for
+     *  the configuration property 
+     *  <code>org.apache.tapestry.engine-class</code>
+     *  occurs (see {@link #searchConfiguration(String)}).
+     * 
+     *  <p>If the search is still unsuccessful, then
+     *  {@link org.apache.tapestry.engine.BaseEngine} is used.
+     * 
+     **/
+
+    protected String getEngineClassName()
+    {
+        if (_engineClassName != null)
+            return _engineClassName;
+
+        _engineClassName = _specification.getEngineClassName();
+
+        if (_engineClassName == null)
+            _engineClassName = searchConfiguration("org.apache.tapestry.engine-class");
+
+        if (_engineClassName == null)
+            _engineClassName = BaseEngine.class.getName();
+
+        return _engineClassName;
+    }
+
+    /**
+     *  Searches for a configuration property in:
+     *  <ul>
+     *  <li>The servlet's initial parameters
+     *  <li>The servlet context's initial parameters
+     *  <li>JVM system properties
+     *  </ul>
+     * 
+     *  @see #createPropertySource()
+     *  @since 3.0
+     * 
+     **/
+
+    protected String searchConfiguration(String propertyName)
+    {
+        if (_propertySource == null)
+            _propertySource = createPropertySource();
+
+        return _propertySource.getPropertyValue(propertyName);
+    }
+
+    /**
+     *  Creates an instance of {@link IPropertySource} used for
+     *  searching of configuration values.  Subclasses may override
+     *  this method if they want to change the normal locations
+     *  that properties are searched for within.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    protected IPropertySource createPropertySource()
+    {
+        DelegatingPropertySource result = new DelegatingPropertySource();
+
+        result.addSource(new ServletPropertySource(getServletConfig()));
+        result.addSource(new ServletContextPropertySource(getServletContext()));
+        result.addSource(SystemPropertiesPropertySource.getInstance());
+
+        return result;
+    }
+
+    /**
+     *  Invoked from the {@link IEngine engine}, just prior to starting to
+     *  render a response, when the locale has changed.  The servlet writes a
+     *  {@link Cookie} so that, on subsequent request cycles, an engine localized
+     *  to the selected locale is chosen.
+     *
+     *  <p>At this time, the cookie is <em>not</em> persistent.  That may
+     *  change in subsequent releases.
+     *
+     *  @since 1.0.1
+     **/
+
+    public void writeLocaleCookie(Locale locale, IEngine engine, RequestContext cycle)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Writing locale cookie " + locale);
+
+        Cookie cookie = new Cookie(LOCALE_COOKIE_NAME, locale.toString());
+        cookie.setPath(engine.getServletPath());
+
+        cycle.addCookie(cookie);
+    }
+
+    /**
+     *  Returns a resource resolver that can access classes and resources related
+     *  to the current web application context.  Relies on
+     *  {@link java.lang.Thread#getContextClassLoader()}, which is set by
+     *  most modern servlet containers.
+     * 
+     *  @since 2.3
+     *
+     **/
+
+    public IResourceResolver getResourceResolver()
+    {
+        return _resolver;
+    }
+
+    /**
+     * Ensures that shared janitor thread is terminated.
+     * @see javax.servlet.Servlet#destroy()
+     * @since 3.0.3
+     */
+    public void destroy()
+    {
+        try
+        {
+            JanitorThread.getSharedJanitorThread().interrupt();
+        }
+        finally
+        {
+            super.destroy();
+        }
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/BaseComponent.java b/tapestry-framework/src/org/apache/tapestry/BaseComponent.java
new file mode 100644
index 0000000..b9f06f4
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/BaseComponent.java
@@ -0,0 +1,139 @@
+//  Copyright 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.tapestry;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.engine.IPageLoader;
+import org.apache.tapestry.engine.IPageSource;
+import org.apache.tapestry.engine.ITemplateSource;
+import org.apache.tapestry.parse.ComponentTemplate;
+import org.apache.tapestry.spec.IComponentSpecification;
+
+/**
+ * Base implementation for most components that use an HTML template.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * 
+ **/
+
+public class BaseComponent extends AbstractComponent
+{
+    private static final Log LOG = LogFactory.getLog(BaseComponent.class);
+
+    private static final int OUTER_INIT_SIZE = 5;
+
+    private IRender[] _outer;
+    private int _outerCount = 0;
+
+    /**
+     *  Adds an element as an outer element for the receiver.  Outer
+     *  elements are elements that should be directly rendered by the
+     *  receiver's <code>render()</code> method.  That is, they are
+     *  top-level elements on the HTML template.
+     *
+     * 
+     **/
+
+    protected void addOuter(IRender element)
+    {
+        if (_outer == null)
+        {
+            _outer = new IRender[OUTER_INIT_SIZE];
+            _outer[0] = element;
+
+            _outerCount = 1;
+            return;
+        }
+
+        // No more room?  Make the array bigger.
+
+        if (_outerCount == _outer.length)
+        {
+            IRender[] newOuter;
+
+            newOuter = new IRender[_outer.length * 2];
+
+            System.arraycopy(_outer, 0, newOuter, 0, _outerCount);
+
+            _outer = newOuter;
+        }
+
+        _outer[_outerCount++] = element;
+    }
+
+    /**
+     *
+     *  Reads the receiver's template and figures out which elements wrap which
+     *  other elements.
+     *
+     *  <P>This is coded as a single, big, ugly method for efficiency.
+     * 
+     **/
+
+    private void readTemplate(IRequestCycle cycle, IPageLoader loader)
+    {
+        IPageSource pageSource = loader.getEngine().getPageSource();
+
+        if (LOG.isDebugEnabled())
+            LOG.debug(this +" reading template");
+
+        ITemplateSource source = loader.getTemplateSource();
+        ComponentTemplate componentTemplate = source.getTemplate(cycle, this);
+
+        // Most of the work is done inside the loader class. 
+        // We instantiate it just to invoke process() on it.
+        
+        new BaseComponentTemplateLoader(cycle, loader, this, componentTemplate, pageSource).process();
+
+        if (LOG.isDebugEnabled())
+            LOG.debug(this +" finished reading template");
+    }
+
+    /**
+     *   Renders the top level components contained by the receiver.
+     *
+     *   @since 2.0.3
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Begin render " + getExtendedId());
+
+        for (int i = 0; i < _outerCount; i++)
+            _outer[i].render(writer, cycle);
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("End render " + getExtendedId());
+    }
+
+    /**
+     *  Loads the template for the component, then invokes
+     *  {@link AbstractComponent#finishLoad(IRequestCycle, IPageLoader, IComponentSpecification)}.
+     *  Subclasses must invoke this method first,
+     *  before adding any additional behavior, though its usually
+     *  simpler to override {@link #finishLoad()} instead.
+     *
+     **/
+
+    public void finishLoad(IRequestCycle cycle, IPageLoader loader, IComponentSpecification specification)
+    {
+        readTemplate(cycle, loader);
+
+        super.finishLoad(cycle, loader, specification);
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/BaseComponentTemplateLoader.java b/tapestry-framework/src/org/apache/tapestry/BaseComponentTemplateLoader.java
new file mode 100644
index 0000000..8150771
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/BaseComponentTemplateLoader.java
@@ -0,0 +1,660 @@
+//  Copyright 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.tapestry;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.binding.ExpressionBinding;
+import org.apache.tapestry.binding.StaticBinding;
+import org.apache.tapestry.binding.StringBinding;
+import org.apache.tapestry.engine.IPageLoader;
+import org.apache.tapestry.engine.IPageSource;
+import org.apache.tapestry.engine.ITemplateSource;
+import org.apache.tapestry.parse.AttributeType;
+import org.apache.tapestry.parse.CloseToken;
+import org.apache.tapestry.parse.ComponentTemplate;
+import org.apache.tapestry.parse.LocalizationToken;
+import org.apache.tapestry.parse.OpenToken;
+import org.apache.tapestry.parse.TemplateAttribute;
+import org.apache.tapestry.parse.TemplateToken;
+import org.apache.tapestry.parse.TextToken;
+import org.apache.tapestry.parse.TokenType;
+import org.apache.tapestry.spec.IComponentSpecification;
+import org.apache.tapestry.spec.IContainedComponent;
+
+/**
+ *  Utility class instantiated by {@link org.apache.tapestry.BaseComponent} to
+ *  process the component's {@link org.apache.tapestry.parse.ComponentTemplate template},
+ *  which involves working through the nested structure of the template and hooking
+ *  the various static template blocks and components together using
+ *  {@link IComponent#addBody(IRender)} and 
+ *  {@link org.apache.tapestry.BaseComponent#addOuter(IRender)}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ */
+
+public class BaseComponentTemplateLoader
+{
+    private static final Log LOG = LogFactory.getLog(BaseComponentTemplateLoader.class);
+
+    private IPageLoader _pageLoader;
+    private IRequestCycle _requestCycle;
+    private BaseComponent _loadComponent;
+    private IPageSource _pageSource;
+    private ComponentTemplate _template;
+    private IComponent[] _stack;
+    private int _stackx = 0;
+    private IComponent _activeComponent = null;
+    private Set _seenIds = new HashSet();
+
+    /**
+     *  A class used with invisible localizations.  Constructed
+     *  from a {@link TextToken}.
+     */
+
+    private static class LocalizedStringRender implements IRender
+    {
+        private IComponent _component;
+        private String _key;
+        private Map _attributes;
+        private String _value;
+        private boolean _raw;
+
+        private LocalizedStringRender(IComponent component, LocalizationToken token)
+        {
+            _component = component;
+            _key = token.getKey();
+            _raw = token.isRaw();
+            _attributes = token.getAttributes();
+        }
+
+        public void render(IMarkupWriter writer, IRequestCycle cycle)
+        {
+            if (cycle.isRewinding())
+                return;
+
+            if (_attributes != null)
+            {
+                writer.begin("span");
+
+                Iterator i = _attributes.entrySet().iterator();
+
+                while (i.hasNext())
+                {
+                    Map.Entry entry = (Map.Entry) i.next();
+                    String attributeName = (String) entry.getKey();
+                    String attributeValue = (String) entry.getValue();
+
+                    writer.attribute(attributeName, attributeValue);
+                }
+            }
+
+            if (_value == null)
+                _value = _component.getMessage(_key);
+
+            if (_raw)
+                writer.printRaw(_value);
+            else
+                writer.print(_value);
+
+            if (_attributes != null)
+                writer.end();
+        }
+
+        public String toString()
+        {
+            ToStringBuilder builder = new ToStringBuilder(this);
+
+            builder.append("component", _component);
+            builder.append("key", _key);
+            builder.append("raw", _raw);
+            builder.append("attributes", _attributes);
+
+            return builder.toString();
+        }
+
+    }
+
+    public BaseComponentTemplateLoader(
+        IRequestCycle requestCycle,
+        IPageLoader pageLoader,
+        BaseComponent loadComponent,
+        ComponentTemplate template,
+        IPageSource pageSource)
+    {
+        _requestCycle = requestCycle;
+        _pageLoader = pageLoader;
+        _loadComponent = loadComponent;
+        _template = template;
+        _pageSource = pageSource;
+
+        _stack = new IComponent[template.getTokenCount()];
+    }
+
+    public void process()
+    {
+        int count = _template.getTokenCount();
+
+        for (int i = 0; i < count; i++)
+        {
+            TemplateToken token = _template.getToken(i);
+
+            TokenType type = token.getType();
+
+            if (type == TokenType.TEXT)
+            {
+                process((TextToken) token);
+                continue;
+            }
+
+            if (type == TokenType.OPEN)
+            {
+                process((OpenToken) token);
+                continue;
+            }
+
+            if (type == TokenType.CLOSE)
+            {
+                process((CloseToken) token);
+                continue;
+            }
+
+            if (type == TokenType.LOCALIZATION)
+            {
+                process((LocalizationToken) token);
+                continue;
+            }
+        }
+
+        // This is also pretty much unreachable, and the message is kind of out
+        // of date, too.
+
+        if (_stackx != 0)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("BaseComponent.unbalance-open-tags"),
+                _loadComponent,
+                null,
+                null);
+
+        checkAllComponentsReferenced();
+    }
+
+    /**
+     *  Adds the token (which implements {@link IRender})
+     *  to the active component (using {@link IComponent#addBody(IRender)}),
+     *  or to this component {@link BaseComponent#addOuter(IRender)}.
+     * 
+     *  <p>
+     *  A check is made that the active component allows a body.
+     */
+
+    private void process(TextToken token)
+    {
+        if (_activeComponent == null)
+        {
+            _loadComponent.addOuter(token);
+            return;
+        }
+
+        if (!_activeComponent.getSpecification().getAllowBody())
+            throw createBodylessComponentException(_activeComponent);
+
+        _activeComponent.addBody(token);
+    }
+
+    private void process(OpenToken token)
+    {
+        String id = token.getId();
+        IComponent component = null;
+        String componentType = token.getComponentType();
+
+        if (componentType == null)
+            component = getEmbeddedComponent(id);
+        else
+        {
+            checkForDuplicateId(id, token.getLocation());
+
+            component = createImplicitComponent(id, componentType, token.getLocation());
+        }
+
+        // Make sure the template contains each component only once.
+
+        if (_seenIds.contains(id))
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "BaseComponent.multiple-component-references",
+                    _loadComponent.getExtendedId(),
+                    id),
+                _loadComponent,
+                token.getLocation(),
+                null);
+
+        _seenIds.add(id);
+
+        if (_activeComponent == null)
+            _loadComponent.addOuter(component);
+        else
+        {
+            // Note: this code may no longer be reachable (because the
+            // template parser does this check first).
+
+            if (!_activeComponent.getSpecification().getAllowBody())
+                throw createBodylessComponentException(_activeComponent);
+
+            _activeComponent.addBody(component);
+        }
+
+        addTemplateBindings(component, token);
+
+        _stack[_stackx++] = _activeComponent;
+
+        _activeComponent = component;
+    }
+
+    private void checkForDuplicateId(String id, ILocation location)
+    {
+        if (id == null)
+            return;
+
+        IContainedComponent cc = _loadComponent.getSpecification().getComponent(id);
+
+        if (cc != null)
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "BaseComponentTemplateLoader.dupe-component-id",
+                    id,
+                    location,
+                    cc.getLocation()),
+                _loadComponent,
+                location,
+                null);
+    }
+
+    private IComponent createImplicitComponent(String id, String componentType, ILocation location)
+    {
+        IComponent result =
+            _pageLoader.createImplicitComponent(
+                _requestCycle,
+                _loadComponent,
+                id,
+                componentType,
+                location);
+
+        return result;
+    }
+
+    private IComponent getEmbeddedComponent(String id)
+    {
+        return _loadComponent.getComponent(id);
+    }
+
+    private void process(CloseToken token)
+    {
+        // Again, this is pretty much impossible to reach because
+        // the template parser does a great job.
+
+        if (_stackx <= 0)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("BaseComponent.unbalanced-close-tags"),
+                _loadComponent,
+                token.getLocation(),
+                null);
+
+        // Null and forget the top element on the stack.
+
+        _stack[_stackx--] = null;
+
+        _activeComponent = _stack[_stackx];
+    }
+
+    private void process(LocalizationToken token)
+    {
+        IRender render = new LocalizedStringRender(_loadComponent, token);
+
+        if (_activeComponent == null)
+            _loadComponent.addOuter(render);
+        else
+            _activeComponent.addBody(render);
+    }
+
+    /**
+     *  Adds bindings based on attributes in the template.
+     */
+
+    private void addTemplateBindings(IComponent component, OpenToken token)
+    {
+        IComponentSpecification spec = component.getSpecification();
+
+        Map attributes = token.getAttributesMap();
+
+        if (attributes != null)
+        {
+            Iterator i = attributes.entrySet().iterator();
+
+            while (i.hasNext())
+            {
+                Map.Entry entry = (Map.Entry) i.next();
+
+                String name = (String) entry.getKey();
+                TemplateAttribute attribute = (TemplateAttribute) entry.getValue();
+                AttributeType type = attribute.getType();
+
+                if (type == AttributeType.OGNL_EXPRESSION)
+                {
+                    addExpressionBinding(
+                        component,
+                        spec,
+                        name,
+                        attribute.getValue(),
+                        token.getLocation());
+                    continue;
+                }
+
+                if (type == AttributeType.LOCALIZATION_KEY)
+                {
+                    addStringBinding(
+                        component,
+                        spec,
+                        name,
+                        attribute.getValue(),
+                        token.getLocation());
+                    continue;
+                }
+
+                if (type == AttributeType.LITERAL)
+                    addStaticBinding(
+                        component,
+                        spec,
+                        name,
+                        attribute.getValue(),
+                        token.getLocation());
+            }
+        }
+
+        // if the component defines a templateTag parameter and 
+        // there is no established binding for that parameter, 
+        // add a static binding carrying the template tag  
+        if (spec.getParameter(ITemplateSource.TEMPLATE_TAG_PARAMETER_NAME) != null
+            && component.getBinding(ITemplateSource.TEMPLATE_TAG_PARAMETER_NAME) == null)
+        {
+            addStaticBinding(
+                component,
+                spec,
+                ITemplateSource.TEMPLATE_TAG_PARAMETER_NAME,
+                token.getTag(),
+                token.getLocation());
+        }
+
+    }
+
+    /**
+     *  Adds an expression binding, checking for errors related
+     *  to reserved and informal parameters.
+     *
+     *  <p>It is an error to specify expression 
+     *  bindings in both the specification
+     *  and the template.
+     */
+
+    private void addExpressionBinding(
+        IComponent component,
+        IComponentSpecification spec,
+        String name,
+        String expression,
+        ILocation location)
+    {
+
+        // If matches a formal parameter name, allow it to be set
+        // unless there's already a binding.
+
+        boolean isFormal = (spec.getParameter(name) != null);
+
+        if (isFormal)
+        {
+            if (component.getBinding(name) != null)
+                throw new ApplicationRuntimeException(
+                    Tapestry.format(
+                        "BaseComponent.dupe-template-expression",
+                        name,
+                        component.getExtendedId(),
+                        _loadComponent.getExtendedId()),
+                    component,
+                    location,
+                    null);
+        }
+        else
+        {
+            if (!spec.getAllowInformalParameters())
+                throw new ApplicationRuntimeException(
+                    Tapestry.format(
+                        "BaseComponent.template-expression-for-informal-parameter",
+                        name,
+                        component.getExtendedId(),
+                        _loadComponent.getExtendedId()),
+                    component,
+                    location,
+                    null);
+
+            // If the name is reserved (matches a formal parameter
+            // or reserved name, caselessly), then skip it.
+
+            if (spec.isReservedParameterName(name))
+                throw new ApplicationRuntimeException(
+                    Tapestry.format(
+                        "BaseComponent.template-expression-for-reserved-parameter",
+                        name,
+                        component.getExtendedId(),
+                        _loadComponent.getExtendedId()),
+                    component,
+                    location,
+                    null);
+        }
+
+        IBinding binding =
+            new ExpressionBinding(
+                _pageSource.getResourceResolver(),
+                _loadComponent,
+                expression,
+                location);
+
+        component.setBinding(name, binding);
+    }
+
+    /**
+      *  Adds an expression binding, checking for errors related
+      *  to reserved and informal parameters.
+      *
+      *  <p>It is an error to specify expression 
+      *  bindings in both the specification
+      *  and the template.
+      */
+
+    private void addStringBinding(
+        IComponent component,
+        IComponentSpecification spec,
+        String name,
+        String localizationKey,
+        ILocation location)
+    {
+        // If matches a formal parameter name, allow it to be set
+        // unless there's already a binding.
+
+        boolean isFormal = (spec.getParameter(name) != null);
+
+        if (isFormal)
+        {
+            if (component.getBinding(name) != null)
+                throw new ApplicationRuntimeException(
+                    Tapestry.format(
+                        "BaseComponent.dupe-string",
+                        name,
+                        component.getExtendedId(),
+                        _loadComponent.getExtendedId()),
+                    component,
+                    location,
+                    null);
+        }
+        else
+        {
+            if (!spec.getAllowInformalParameters())
+                throw new ApplicationRuntimeException(
+                    Tapestry.format(
+                        "BaseComponent.template-expression-for-informal-parameter",
+                        name,
+                        component.getExtendedId(),
+                        _loadComponent.getExtendedId()),
+                    component,
+                    location,
+                    null);
+
+            // If the name is reserved (matches a formal parameter
+            // or reserved name, caselessly), then skip it.
+
+            if (spec.isReservedParameterName(name))
+                throw new ApplicationRuntimeException(
+                    Tapestry.format(
+                        "BaseComponent.template-expression-for-reserved-parameter",
+                        name,
+                        component.getExtendedId(),
+                        _loadComponent.getExtendedId()),
+                    component,
+                    location,
+                    null);
+        }
+
+        IBinding binding = new StringBinding(_loadComponent, localizationKey, location);
+
+        component.setBinding(name, binding);
+    }
+
+    /**
+     *  Adds a static binding, checking for errors related
+     *  to reserved and informal parameters.
+     * 
+     *  <p>
+     *  Static bindings that conflict with bindings in the
+     *  specification are quietly ignored.
+     */
+
+    private void addStaticBinding(
+        IComponent component,
+        IComponentSpecification spec,
+        String name,
+        String staticValue,
+        ILocation location)
+    {
+
+        if (component.getBinding(name) != null)
+            return;
+
+        // If matches a formal parameter name, allow it to be set
+        // unless there's already a binding.
+
+        boolean isFormal = (spec.getParameter(name) != null);
+
+        if (!isFormal)
+        {
+            // Skip informal parameters if the component doesn't allow them.
+
+            if (!spec.getAllowInformalParameters())
+                return;
+
+            // If the name is reserved (matches a formal parameter
+            // or reserved name, caselessly), then skip it.
+
+            if (spec.isReservedParameterName(name))
+                return;
+        }
+
+        IBinding binding = new StaticBinding(staticValue, location);
+
+        component.setBinding(name, binding);
+    }
+
+    private void checkAllComponentsReferenced()
+    {
+        // First, contruct a modifiable copy of the ids of all expected components
+        // (that is, components declared in the specification).
+
+        Map components = _loadComponent.getComponents();
+
+        Set ids = components.keySet();
+
+        // If the seen ids ... ids referenced in the template, matches
+        // all the ids in the specification then we're fine.
+
+        if (_seenIds.containsAll(ids))
+            return;
+
+        // Create a modifiable copy.  Remove the ids that are referenced in
+        // the template.  The remainder are worthy of note.
+
+        ids = new HashSet(ids);
+        ids.removeAll(_seenIds);
+
+        int count = ids.size();
+
+        String key =
+            (count == 1)
+                ? "BaseComponent.missing-component-spec-single"
+                : "BaseComponent.missing-component-spec-multi";
+
+        StringBuffer buffer =
+            new StringBuffer(Tapestry.format(key, _loadComponent.getExtendedId()));
+
+        Iterator i = ids.iterator();
+        int j = 1;
+
+        while (i.hasNext())
+        {
+            if (j == 1)
+                buffer.append(' ');
+            else
+                if (j == count)
+                {
+                    buffer.append(' ');
+                    buffer.append(Tapestry.getMessage("BaseComponent.and"));
+                    buffer.append(' ');
+                }
+                else
+                    buffer.append(", ");
+
+            buffer.append(i.next());
+
+            j++;
+        }
+
+        buffer.append('.');
+
+        LOG.error(buffer.toString());
+    }
+
+    protected ApplicationRuntimeException createBodylessComponentException(IComponent component)
+    {
+        return new ApplicationRuntimeException(
+            Tapestry.getMessage("BaseComponentTemplateLoader.bodyless-component"),
+            component,
+            null,
+            null);
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/BindingException.java b/tapestry-framework/src/org/apache/tapestry/BindingException.java
new file mode 100644
index 0000000..094b274
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/BindingException.java
@@ -0,0 +1,61 @@
+//  Copyright 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.tapestry;
+
+/**
+ *  A general exception describing an {@link IBinding}
+ *  and an {@link IComponent}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+public class BindingException extends ApplicationRuntimeException
+{
+    private transient IBinding _binding;
+
+    public BindingException(String message, IBinding binding)
+    {
+        this(message, binding, null);
+    }
+
+    public BindingException(String message, IBinding binding, Throwable rootCause)
+    {
+        this(message, null, null, binding, rootCause);
+    }
+
+
+    public BindingException(
+        String message,
+        Object component,
+        ILocation location,
+        IBinding binding,
+        Throwable rootCause)
+    {
+        super(
+            message,
+            component,
+            Tapestry.findLocation(new Object[] { location, binding, component }),
+            rootCause);
+
+        _binding = binding;
+    }
+
+    public IBinding getBinding()
+    {
+        return _binding;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/ConfigurationDefaults.properties b/tapestry-framework/src/org/apache/tapestry/ConfigurationDefaults.properties
new file mode 100644
index 0000000..8451a55
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/ConfigurationDefaults.properties
@@ -0,0 +1,4 @@
+# $Id$
+
+org.apache.tapestry.default-script-language=jython
+org.apache.tapestry.visit-class=java.util.HashMap
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/Framework.library b/tapestry-framework/src/org/apache/tapestry/Framework.library
new file mode 100644
index 0000000..b8f4d11
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/Framework.library
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE library-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<!--
+ 
+   This is a kind of "boostrap" library; when an unadorned page name or alias (a name or
+   alias without a namespace prefix) cannot be located in the appropriate application or
+   library specification, the framework specification is checked.  This allows
+   the application or library to override framework's versions of a component.
+   
+-->
+
+<library-specification>
+
+    <component-type type="ActionLink" specification-path="link/ActionLink.jwc"/>
+    <component-type type="Any" specification-path="components/Any.jwc"/>
+    <component-type type="Block" specification-path="components/Block.jwc"/>
+    <component-type type="Body" specification-path="html/Body.jwc"/>
+    <component-type type="Button" specification-path="form/Button.jwc"/>
+    <component-type type="Checkbox" specification-path="form/Checkbox.jwc"/>
+    <component-type type="Conditional" specification-path="components/Conditional.jwc"/>
+    <component-type type="DatePicker" specification-path="form/DatePicker.jwc"/>
+    <component-type type="Delegator" specification-path="components/Delegator.jwc"/>
+    <component-type type="DirectLink" specification-path="link/DirectLink.jwc"/>
+    <component-type type="ExternalLink" specification-path="link/ExternalLink.jwc"/>
+    <component-type type="FieldLabel" specification-path="valid/FieldLabel.jwc"/>
+    <component-type type="Foreach" specification-path="components/Foreach.jwc"/>
+    <component-type type="Frame" specification-path="html/Frame.jwc"/>
+    <component-type type="ExceptionDisplay" specification-path="html/ExceptionDisplay.jwc"/>
+    <component-type type="Form" specification-path="form/Form.jwc"/>
+    <component-type type="GenericLink" specification-path="link/GenericLink.jwc"/>
+    <component-type type="Hidden" specification-path="form/Hidden.jwc"/>
+    <component-type type="Image" specification-path="html/Image.jwc"/>
+    <component-type type="ImageSubmit" specification-path="form/ImageSubmit.jwc"/>
+	<component-type type="Insert" specification-path="components/Insert.jwc"/>
+    <component-type type="InsertText" specification-path="html/InsertText.jwc"/>
+    <component-type type="LinkSubmit" specification-path="form/LinkSubmit.jwc"/>
+    <component-type type="ListEdit" specification-path="form/ListEdit.jwc"/>
+    <component-type type="Option" specification-path="form/Option.jwc"/>
+    <component-type type="PageLink" specification-path="link/PageLink.jwc"/>
+    <component-type type="PropertySelection" specification-path="form/PropertySelection.jwc"/>
+    <component-type type="Radio" specification-path="form/Radio.jwc"/>
+    <component-type type="RadioGroup" specification-path="form/RadioGroup.jwc"/>
+    <component-type type="RenderBlock" specification-path="components/RenderBlock.jwc"/>
+    <component-type type="RenderBody" specification-path="components/RenderBody.jwc"/>
+    <component-type type="Rollover" specification-path="html/Rollover.jwc"/>
+    <component-type type="Select" specification-path="form/Select.jwc"/>
+    <component-type type="ServiceLink" specification-path="link/ServiceLink.jwc"/>
+    <component-type type="Script" specification-path="html/Script.jwc"/>
+    <component-type type="Shell" specification-path="html/Shell.jwc"/>
+    <component-type type="Submit" specification-path="form/Submit.jwc"/>
+    <component-type type="TextArea" specification-path="form/TextArea.jwc"/>
+    <component-type type="TextField" specification-path="form/TextField.jwc"/>
+    <component-type type="Upload" specification-path="form/Upload.jwc"/>
+    <component-type type="ValidField" specification-path="valid/ValidField.jwc"/>
+          
+    <page name="StaleLink" specification-path="pages/StaleLink.page"/>
+    <page name="StaleSession" specification-path="pages/StaleSession.page"/>
+    <page name="Exception" specification-path="pages/Exception.page"/>
+   
+    <page name="WMLException" specification-path="wml/pages/WMLException.page"/>
+    <page name="WMLStaleLink" specification-path="wml/pages/WMLStaleLink.page"/>
+    <page name="WMLStaleSession" specification-path="wml/pages/WMLStaleSession.page"/>
+
+    <!-- These may be overriden but generally shouldn't be, except perhaps for the home service. -->
+    
+    <service name="home" class="org.apache.tapestry.engine.HomeService"/>
+    <service name="action" class="org.apache.tapestry.engine.ActionService"/>
+    <service name="direct" class="org.apache.tapestry.engine.DirectService"/>
+    <service name="page" class="org.apache.tapestry.engine.PageService"/>
+    <service name="reset" class="org.apache.tapestry.engine.ResetService"/>
+    <service name="restart" class="org.apache.tapestry.engine.RestartService"/>
+    <service name="asset" class="org.apache.tapestry.asset.AssetService"/>
+	<service name="external" class="org.apache.tapestry.engine.ExternalService"/>
+	
+	<!-- Used to support the JSP tags. -->
+	
+	<service name="tagsupport" class="org.apache.tapestry.engine.TagSupportService"/>
+	
+</library-specification>
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/IAction.java b/tapestry-framework/src/org/apache/tapestry/IAction.java
new file mode 100644
index 0000000..1c089aa
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/IAction.java
@@ -0,0 +1,37 @@
+//  Copyright 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.tapestry;
+
+/**
+ *  A particular type of component usuable with the
+ *  action service.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.1
+ */
+
+public interface IAction extends IComponent
+{
+    /**
+     *  Returns true if the component requires 
+     *  an existing, not new, {@link javax.servlet.http.HttpSession}
+     *  to operate.  Components who are not dependant on page state
+     *  (or the visit object) are non-stateful and can return false.
+     *
+     **/
+
+    public boolean getRequiresSession();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/IActionListener.java b/tapestry-framework/src/org/apache/tapestry/IActionListener.java
new file mode 100644
index 0000000..0a59bac
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/IActionListener.java
@@ -0,0 +1,42 @@
+//  Copyright 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.tapestry;
+
+/**
+ *  Defines a listener to an {@link IAction} component, which is way to
+ *  get behavior when the component's URL is triggered (or the form
+ *  containing the component is submitted).  Certain form elements 
+ *  ({@link org.apache.tapestry.form.Hidden})
+ *  also make use of this interface.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+public interface IActionListener
+{
+
+    /**
+     *  Method invoked by the component (an {@link org.apache.tapestry.link.ActionLink} or 
+     *  {@link org.apache.tapestry.form.Form}, when its URL is triggered.
+     *
+     *  @param component The component which was "triggered".
+     *  @param cycle The request cycle in which the component was triggered.
+     *
+     **/
+
+    public void actionTriggered(IComponent component, IRequestCycle cycle);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/IAsset.java b/tapestry-framework/src/org/apache/tapestry/IAsset.java
new file mode 100644
index 0000000..ef8d24a
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/IAsset.java
@@ -0,0 +1,65 @@
+//  Copyright 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.tapestry;
+
+import java.io.InputStream;
+
+/**
+ *  Representation of a asset (GIF, JPEG, etc.) that may be owned by a
+ *  {@link IComponent}.
+ *
+ *  <p>Assets may be completely external (i.e., on some other web site), 
+ *  contained by the {@link javax.servlet.ServletContext},  
+ *  or stored somewhere in the classpath.
+ *
+ *  <p>In the latter two cases, the resource may be localized.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+public interface IAsset extends ILocatable
+{
+    /**
+     *  Returns a URL for the asset, ready to be inserted into the output HTML.
+     *  If the asset can be localized, the localized version (matching the
+     *  {@link java.util.Locale} of the current {@link IPage page}) is returned.
+     * 
+     *  @throws ApplicationRuntimeException if the asset does not exist.
+     *
+     **/
+
+    public String buildURL(IRequestCycle cycle);
+
+    /**
+     *  Accesses the localized version of the resource (if possible) and returns it as
+     *  an input stream.  A version of the resource localized to the
+     *  current {@link IPage page} is returned.
+     * 
+     *  @throws ApplicationRuntimeException if the asset does not exist, or
+     *  can't be read.
+     *
+     **/
+
+    public InputStream getResourceAsStream(IRequestCycle cycle);
+    
+    /**
+     *  Returns the underlying location of the asset.
+     * 
+     **/
+    
+    public IResourceLocation getResourceLocation();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/IBeanProvider.java b/tapestry-framework/src/org/apache/tapestry/IBeanProvider.java
new file mode 100644
index 0000000..4c39b93
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/IBeanProvider.java
@@ -0,0 +1,82 @@
+//  Copyright 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.tapestry;
+
+import java.util.Collection;
+
+/**
+ *  An object that provides a component with access to helper beans.
+ *  Helper beans are JavaBeans associated with a page or component
+ *  that are used to extend the functionality of the component via
+ *  aggregation.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.4
+ **/
+
+
+public interface IBeanProvider
+{
+	/**
+	 *  Returns the JavaBean with the specified name.  The bean is created as needed.
+	 *
+	 *  @throws ApplicationRuntimeException if no such bean is available.
+	 *
+	 **/
+	
+	public Object getBean(String name);
+	
+	/**
+	 *  Returns the {@link IComponent} (which may be a 
+	 *  {@link org.apache.tapestry.IPage}) for which
+	 *  this bean provider is providing beans.
+	 *
+	 *  @since 1.0.5
+	 **/
+	
+	public IComponent getComponent();
+	
+	/**
+	 *  Returns a collection of the names of any beans which may
+	 *  be provided.
+	 *
+	 *  @since 1.0.6
+	 *  @see org.apache.tapestry.spec.IComponentSpecification#getBeanNames()
+	 *
+	 **/
+	
+	public Collection getBeanNames();
+	
+    /**
+     *  Returns true if the provider can provide the named bean.
+     * 
+     *  @since 2.2
+     * 
+     **/
+    
+    public boolean canProvideBean(String name);
+    
+	/**
+	 *  Returns a resource resolver.
+	 * 
+	 *  @since 1.0.8
+	 * 
+	 **/
+	
+	public IResourceResolver getResourceResolver();
+	
+}
+
diff --git a/tapestry-framework/src/org/apache/tapestry/IBinding.java b/tapestry-framework/src/org/apache/tapestry/IBinding.java
new file mode 100644
index 0000000..c0a986a
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/IBinding.java
@@ -0,0 +1,157 @@
+//  Copyright 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.tapestry;
+
+/**
+ *  A binding is the mechanism used to provide values for parameters of
+ *  specific {@link IComponent} instances. The component doesn't
+ *  care where the required value comes from, it simply requires that
+ *  a value be provided when needed.
+ *
+ *  <p>Bindings are set inside the containing component's specification.
+ *  Bindings may be static or dynamic (though that is irrelevant to the
+ *  component).  Components may also use a binding to write a value
+ *  back through a property to some other object (typically, another component).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ **/
+
+public interface IBinding extends ILocatable
+{
+    /**
+     *  Invokes {@link #getObject()}, then coerces the value to a boolean.  
+     *  The following rules are used to perform the coercion:
+     *  <ul>
+     *  <li>null is always false
+     *  <li>A {@link Boolean} value is self-evident
+     *  <li>A {@link Number} value is true if non-zero
+     *  <li>A {@link String} value is true if non-empty and contains
+     *  non-whitespace characters
+     *  <li>Any {@link java.util.Collection} value is true if it has a non-zero
+     *  {@link java.util.Collection#size() size}
+     *  <li>Any array type is true if it has a non-zero length
+     *  <li>Any other non-null value is true
+     *  </ul>
+     * 
+     *  @see Tapestry#evaluateBoolean(Object)
+     * 
+     **/
+
+    public boolean getBoolean();
+
+    /**
+     *  Gets the value of the Binding using {@link #getObject} and coerces it
+     *  to an <code>int</code>.  Strings will be parsed, and other
+     *  <code>java.lang.Number</code> classes will have <code>intValue()</code>
+     *  invoked.  
+     *
+     *  @throws ClassCastException if the binding's value is not of a usable class.
+     *  @throws BindingException if the binding's value is null.
+     **/
+
+    public int getInt();
+
+    /**
+     *  Gets the value of the Binding using {@link #getObject()} and coerces it
+     *  to a <code>double</code>.  Strings will be parsed, and other
+     *  <code>java.lang.Number</code> classes will have <code>doubleValue()</code>
+     *  invoked.
+     *
+     *  @throws ClassCastException if the binding's value is not of a usable class.
+     *  @throws BindingException if the binding's value is null.
+     **/
+
+    public double getDouble();
+
+    /**
+     *  Invokes {@link #getObject()} and converts the result to <code>java.lang.String</code>.
+     **/
+
+    public String getString();
+
+    /**
+     *  Returns the value of this binding.  This is the essential method.  Other methods
+     *  get this value and cast or coerce the value.
+     *
+     **/
+
+    public Object getObject();
+
+    /**
+     *  Returns the value for the binding after performing some basic checks.
+     *
+     *  @param parameterName the name of the parameter (used to build
+     *  the message if an exception is thrown).
+     *  @param type if not null, the value must be assignable to the specific
+     *  class
+     *  @throws BindingException if the value is not assignable to the
+     *  specified type
+     *
+     *  @since 0.2.9
+     **/
+
+    public Object getObject(String parameterName, Class type);
+
+    /**
+     *  Returns true if the value is invariant (not changing; the
+     *  same value returned each time).  Static and field bindings
+     *  are always invariant, and {@link org.apache.tapestry.binding.ExpressionBinding}s
+     *  may be marked invariant (as an optimization).
+     * 
+     *  @since 2.0.3
+     * 
+     **/
+
+    public boolean isInvariant();
+
+    /**
+     *  Constructs a <code>Boolean</code> and invokes {@link #setObject(Object)}.
+     *
+     **/
+
+    public void setBoolean(boolean value);
+
+    /**
+     *  Constructs an <code>Integer</code> and invokes {@link #setObject(Object)}.
+     *
+     **/
+
+    public void setInt(int value);
+
+    /**
+     *  Constructs an <code>Double</code> and invokes {@link #setObject(Object)}.
+     *
+     **/
+
+    public void setDouble(double value);
+
+    /**
+     *  Invokes {@link #setObject(Object)}.
+     *
+     **/
+
+    public void setString(String value);
+
+    /**
+     *  Updates the value of the binding, if possible.
+     *
+     *  @exception BindingException If the binding is read only.
+     *
+     **/
+
+    public void setObject(Object value);
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/IComponent.java b/tapestry-framework/src/org/apache/tapestry/IComponent.java
new file mode 100644
index 0000000..5f3b8dd
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/IComponent.java
@@ -0,0 +1,354 @@
+//  Copyright 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.tapestry;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.apache.tapestry.engine.IPageLoader;
+import org.apache.tapestry.spec.IComponentSpecification;
+
+/**
+ *  Defines an object which may be used to provide dynamic content on a Tapestry web page.
+ *
+ *  <p>Components are created dynamically from thier class names (part of the
+ *  {@link IComponentSpecification}).
+ *
+ *
+ *  @author Howard Leiws Ship
+ *  @version $Id$
+ * 
+ **/
+
+public interface IComponent extends IRender, ILocationHolder
+{
+
+    /**
+     *  Adds an asset to the component.  This is invoked from the page loader.
+     *
+     **/
+
+    public void addAsset(String name, IAsset asset);
+
+    /**
+     *  Adds a component to a container.  Should only be called during the page
+     *  loading process, which is responsible for any checking.
+     *
+     *  @see IPageLoader
+     *
+     **/
+
+    public void addComponent(IComponent component);
+
+    /**
+     *  Adds a new renderable element to the receiver's body.  The element may be either
+     *  another component, or static HTML.  Such elements come from inside
+     *  the receiver's tag within its container's template, and represent static
+     *  text and other components.
+     * 
+     *  <p>The method {@link #renderBody(IMarkupWriter, IRequestCycle)} is used
+     *  to render these elements.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public void addBody(IRender element);
+
+    /**
+     *  Returns the asset map for the component, which may be empty but will not be null.
+     *
+     *  <p>The return value is unmodifiable.
+     * 
+     **/
+
+    public Map getAssets();
+
+    /**
+     *  Returns the named asset, or null if not found.
+     *
+     **/
+
+    public IAsset getAsset(String name);
+
+    /**
+     *  Returns the binding with the given name or null if not found.
+     *
+     *  <p>Bindings are added to a component using {@link #setBinding(String,IBinding)}.
+     **/
+
+    public IBinding getBinding(String name);
+
+    /**
+     *  Returns a {@link Collection} of the names of all bindings (which includes
+     *  bindings for both formal and informal parameters).
+     *
+     *  <p>The return value is unmodifiable.  It will be null for a {@link IPage page},
+     *  or may simply be empty for a component with no bindings.
+     *
+     **/
+
+    public Collection getBindingNames();
+
+    /**
+     *  Returns a {@link Map} of the {@link IBinding bindings} for this component; 
+     *  this includes informal parameters
+     *  as well as formal bindings.
+     *
+     *  @since 1.0.5
+     *
+     **/
+
+    public Map getBindings();
+
+    /**
+     *  Retrieves an contained component by its id.
+     *  Contained components have unique ids within their container.
+     *
+     *  @exception ApplicationRuntimeException runtime exception thrown if the named
+     *  component does not exist.
+     *
+     **/
+
+    public IComponent getComponent(String id);
+
+    /**
+     *  Returns the component which embeds the receiver.  All components are contained within
+     *  other components, with the exception of the root page component.
+     *
+     *  <p>A page returns null.
+     *
+     **/
+
+    public IComponent getContainer();
+
+    /**
+     *  Sets the container of the component.    This is write-once,
+     *  an attempt to change it later will throw an {@link ApplicationRuntimeException}.
+     *
+     **/
+
+    public void setContainer(IComponent value);
+
+    /**
+     *  Returns a string identifying the name of the page and the id path of the reciever within
+     *  the page.  Pages simply return their name.
+     *
+     *  @see #getIdPath()
+     **/
+
+    public String getExtendedId();
+
+    /**
+     *  Returns the simple id of the component, as defined in its specification.
+     *
+     *  <p>An id will be unique within the
+     *  component which contains this component.
+     *
+     *  <p>A  {@link IPage page} will always return null.
+     *
+     **/
+
+    public String getId();
+
+    /**
+     *  Sets the id of the component.    This is write-once,
+     *  an attempt to change it later will throw an {@link ApplicationRuntimeException}.
+     *
+     **/
+
+    public void setId(String value);
+
+    /**
+     *  Returns the qualified id of the component.  This represents a path from the
+     *  {@link IPage page} to
+     *  this component, showing how components contain each other.
+     *
+     *  <p>A {@link IPage page} will always return
+     *  null.  A component contained on a page returns its simple id.
+     *  Other components return their container's id path followed by a period and their
+     *  own name.
+     *
+     *  @see #getId()
+     **/
+
+    public String getIdPath();
+
+    /**
+     *  Returns the page which ultimately contains the receiver.  A page will return itself.
+     *
+     **/
+
+    public IPage getPage();
+
+    /**
+     *  Sets the page which ultimiately contains the component.  This is write-once,
+     *  an attempt to change it later will throw an {@link ApplicationRuntimeException}.
+     *
+     **/
+
+    public void setPage(IPage value);
+
+    /**
+     *  Returns the specification which defines the component.
+     *
+     **/
+
+    public IComponentSpecification getSpecification();
+
+    /**
+     *  Sets the specification used by the component.  This is write-once, an attempt
+     *  to change it later will throw an {@link ApplicationRuntimeException}.
+     *
+     **/
+
+    public void setSpecification(IComponentSpecification value);
+
+    /**
+     *  Invoked to make the receiver render its body (the elements and components
+     *  its tag wraps around, on its container's template).  
+     *  This method is public so that the
+     *  {@link org.apache.tapestry.components.RenderBody} component may operate.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public void renderBody(IMarkupWriter writer, IRequestCycle cycle);
+
+    /**
+     *  Adds a binding to a container.  Should only be called during the page
+     *  loading process (which is responsible for eror checking).
+     *
+     *  @see IPageLoader
+     *
+     **/
+
+    public void setBinding(String name, IBinding binding);
+
+    /**
+     *  Returns the contained components as an unmodifiable {@link Map}.  This
+     *  allows peer components to work together without directly involving their
+     *  container ... the classic example is to have an {@link org.apache.tapestry.components.Insert} 
+     *  work with an enclosing {@link org.apache.tapestry.components.Foreach}.
+     *
+     *  <p>This is late addition to Tapestry, because it also opens the door
+     *  to abuse, since it is quite possible to break the "black box" aspect of
+     *  a component by interacting directly with components it embeds.  This creates
+     *  ugly interelationships between components that should be seperated.
+     *
+     *  @return A Map of components keyed on component id.  May return an empty map, but won't return
+     *  null.
+     *
+     **/
+
+    public Map getComponents();
+
+    /**
+     *  Allows a component to finish any setup after it has been constructed.
+     *
+     *  <p>The exact timing is not
+     *  specified, but any components contained by the
+     *  receiving component will also have been constructed
+     *  before this method is invoked.
+     *
+     *  <p>As of release 1.0.6, this method is invoked <em>before</em>
+     *  bindings are set.  This should not affect anything, as bindings
+     *  should only be used during renderring.
+     * 
+     *  <p>Release 2.2 added the cycle parameter which is, regretfully, not
+     *  backwards compatible.
+     *
+     *  @since 0.2.12
+     * 
+     **/
+
+    public void finishLoad(
+        IRequestCycle cycle,
+        IPageLoader loader,
+        IComponentSpecification specification);
+
+    /**
+     *  Returns a localized string message.  Each component has an optional
+     *  set of localized message strings that are read from properties
+     *  files.
+     * 
+     *  @param key the key used to locate the message
+     *  @return the localized message for the key, or a placeholder
+     *  if no message is defined for the key.
+     * 
+     *  @since 2.0.4
+     *  @deprecated To be removed in 3.1, use {@link #getMessage(String)}.
+     **/
+
+    public String getString(String key);
+
+    /**
+     *  Returns a localized string message.  Each component has an optional
+     *  set of localized message strings that are read from properties
+     *  files.
+     * 
+     *  @param key the key used to locate the message
+     *  @return the localized message for the key, or a placeholder
+     *  if no message is defined for the key.
+     * 
+     *  @since 3.0
+     **/
+
+    public String getMessage(String key);
+    /**
+     *  Returns component strings for the component.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    public IMessages getMessages();
+
+    /**
+     *  Returns the {@link INamespace} in which the component was defined
+     *  (as an alias).  
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public INamespace getNamespace();
+
+    /**
+     *  Sets the {@link INamespace} for the component.  The namespace
+     *  should only be set once.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public void setNamespace(INamespace namespace);
+    
+    /**
+     *  Sets a property of a component.
+     *  @param propertyName the property name
+     *  @param value the provided value
+     */
+	public void setProperty(String propertyName, Object value);
+	
+	/**
+	 *  Gets a property of a component.
+	 *  @param propertyName the property name
+	 *  @return Object the value of property
+	 */
+	public Object getProperty(String propertyName);
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/IDirect.java b/tapestry-framework/src/org/apache/tapestry/IDirect.java
new file mode 100644
index 0000000..f4c95c6
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/IDirect.java
@@ -0,0 +1,49 @@
+//  Copyright 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.tapestry;
+
+/**
+ *  Interface that defines classes that may be messaged by the direct
+ *  service.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ **/
+
+public interface IDirect extends IComponent
+{
+    /**
+     *  Invoked by the direct service to have the component peform
+     *  the appropriate action.  The {@link org.apache.tapestry.link.DirectLink} component will
+     *  notify its listener.
+     *
+     **/
+
+    public void trigger(IRequestCycle cycle);
+
+    /**
+     *  Invoked by the direct service to query the component as to
+     *  whether it is stateful.  If stateful and no 
+     *  {@link javax.servlet.http.HttpSession} is active, then a
+     *  {@link org.apache.tapestry.StaleSessionException} is
+     *  thrown by the service.
+     * 
+     *  @since 2.3
+     * 
+     **/
+
+    public boolean isStateful();
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/IEngine.java b/tapestry-framework/src/org/apache/tapestry/IEngine.java
new file mode 100644
index 0000000..c340012
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/IEngine.java
@@ -0,0 +1,386 @@
+//  Copyright 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.tapestry;
+
+import java.io.IOException;
+import java.util.Locale;
+
+import javax.servlet.ServletException;
+
+import org.apache.tapestry.asset.ResourceChecksumSource;
+import org.apache.tapestry.engine.IComponentClassEnhancer;
+import org.apache.tapestry.engine.IComponentMessagesSource;
+import org.apache.tapestry.engine.IEngineService;
+import org.apache.tapestry.engine.IPageRecorder;
+import org.apache.tapestry.engine.IPageSource;
+import org.apache.tapestry.engine.IPropertySource;
+import org.apache.tapestry.engine.IScriptSource;
+import org.apache.tapestry.engine.ISpecificationSource;
+import org.apache.tapestry.engine.ITemplateSource;
+import org.apache.tapestry.request.RequestContext;
+import org.apache.tapestry.spec.IApplicationSpecification;
+import org.apache.tapestry.util.io.DataSqueezer;
+import org.apache.tapestry.util.pool.Pool;
+
+/**
+ * Defines the core, session-persistant object used to run a Tapestry
+ * application for a single client (each client will have its own instance of the engine).
+ *
+ * <p>The engine exists to provide core services to the pages and components
+ * that make up the application.  The engine is a delegate to the
+ * {@link ApplicationServlet} via the {@link #service(RequestContext)} method.
+ *
+ * <p>Engine instances are persisted in the {@link javax.servlet.http.HttpSession} and are serializable.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ **/
+
+public interface IEngine
+{
+    /**
+     *  The name ("Home") of the default page presented when a user first accesses the
+     *  application.
+     *
+     *  @see org.apache.tapestry.engine.HomeService
+     * 
+     **/
+
+    public static final String HOME_PAGE = "Home";
+
+    /**
+     *  The name ("Exception") of the page used for reporting exceptions.
+     *  
+     *  <p>Such a page must have
+     *  a writable JavaBeans property named 'exception' of type 
+     * <code>java.lang.Throwable</code>.
+     *
+     **/
+
+    public static final String EXCEPTION_PAGE = "Exception";
+
+    /**
+     *  The name ("StaleLink") of the page used for reporting stale links.
+     *
+     *  <p>The page must implement a writeable JavaBeans proeprty named
+     *  'message' of type <code>String</code>.
+     *
+     **/ 
+
+    public static final String STALE_LINK_PAGE = "StaleLink";
+    
+    /**
+     *  Returns a recorder for a page.  Returns null if the page recorder has
+     *  not been created yet.
+     *
+     *  @see #createPageRecorder(String, IRequestCycle)
+     * 
+     **/
+
+    public IPageRecorder getPageRecorder(String pageName, IRequestCycle cycle);
+
+    /**
+     *  The name ("StaleSession") of the page used for reporting state sessions.
+     *
+     **/
+
+    public static final String STALE_SESSION_PAGE = "StaleSession";
+
+    /**
+     *  Forgets changes to the named page by discarding the page recorder for the page.
+     *  This is used when transitioning from one part
+     *  of an application to another.  All property changes for the page are lost.
+     *
+     *  <p>This should be done if the page is no longer needed or relevant, otherwise
+     *  the properties for the page will continue to be recorded by the engine, which
+     *  is wasteful (especially if clustering or failover is employed on the application).
+     *
+     *  <p>Throws an {@link ApplicationRuntimeException} if there are uncommitted changes
+     *  for the recorder (in the current request cycle).
+     *
+     **/
+
+    public void forgetPage(String name);
+
+    /**
+     *  Returns the locale for the engine.  This locale is used when selecting
+     *  templates and assets.
+     **/
+
+    public Locale getLocale();
+
+    /**
+     *  Changes the engine's locale.  Any subsequently loaded pages will be
+     *  in the new locale (though pages already loaded stay in the old locale).
+     *  Generally, you should render a new page after changing the locale, to
+     *  show that the locale has changed.
+     *
+     **/
+
+    public void setLocale(Locale value);
+
+
+
+    /**
+     *  Creates a new page recorder for the named page.
+     *
+     **/
+
+    public IPageRecorder createPageRecorder(String pageName, IRequestCycle cycle);
+
+    /**
+     *  Returns the object used to load a page from its specification.
+     *
+     **/
+
+    public IPageSource getPageSource();
+
+    /**
+     *  Gets the named service, or throws an {@link
+     *  org.apache.tapestry.ApplicationRuntimeException} 
+     *  if the application can't provide
+     *  the named server.
+     *
+     *  <p>The object returned has a short lifecycle (it isn't
+     *  serialized with the engine).  Repeated calls with the
+     *  same name are not guarenteed to return the same object,
+     *  especially in different request cycles.
+     *
+     **/
+
+    public IEngineService getService(String name);
+
+    /**
+     *  Returns the URL path that corresponds to the servlet for the application.  
+     *  This is required by instances of {@link IEngineService} that need 
+     *  to construct URLs for the application.  This value will include
+     *  the context path.
+     **/
+
+    public String getServletPath();
+
+    /**
+     *  Returns the context path, a string which is prepended to the names of
+     *  any assets or servlets.  This may be the empty string, but won't be null.
+     *
+     *  <p>This value is obtained from 
+     *  {@link javax.servlet.http.HttpServletRequest#getContextPath()}.
+     * 
+     **/
+
+    public String getContextPath();
+
+    /**
+     *  Returns the application specification that defines the application
+     *  and its pages.
+     *
+     **/
+
+    public IApplicationSpecification getSpecification();
+
+    /**
+     *  Returns the source of all component specifications for the application.  
+     *  The source is shared between sessions.
+     *
+     *  @see org.apache.tapestry.engine.AbstractEngine#createSpecificationSource(RequestContext)
+     * 
+     **/
+
+    public ISpecificationSource getSpecificationSource();
+
+    /**
+     *  Returns the source for HTML templates.
+     *
+     *  @see  org.apache.tapestry.engine.AbstractEngine#createTemplateSource(RequestContext)
+     * 
+     **/
+
+    public ITemplateSource getTemplateSource();
+
+    /**
+     *  Method invoked from the {@link org.apache.tapestry.ApplicationServlet} 
+     *  to perform processing of the
+     *  request.  In release 3.0, this has become more of a dirty flag, indicating
+     *  if any state stored by the engine instance itself has changed.
+     *
+     *  @return true if the state of the engine was, or could have been, changed during
+     *  processing.
+     *
+     **/
+
+    public boolean service(RequestContext context) throws ServletException, IOException;
+
+    /**
+     *  Returns an object that can resolve resources and classes.
+     *
+     **/
+
+    public IResourceResolver getResourceResolver();
+
+    /**
+     *  Returns the visit object, an object that represents the client's visit
+     *  to the application.  This is where most server-side state is stored (with
+     *  the exception of persistent page properties).
+     *
+     *  <p>Returns the visit, if it exists, or null if it has not been created.
+     *
+     **/
+
+    public Object getVisit();
+
+    /**
+     *  Returns the visit object, creating it if necessary.
+     *
+     **/
+
+    public Object getVisit(IRequestCycle cycle);
+
+    /**
+     *  Allows the visit object to be removed; typically done when "shutting down"
+     *  a user's session (by setting the visit to null).
+     *
+     **/
+
+    public void setVisit(Object value);
+
+    /**
+     *  Returns the globally shared application object. The global object is
+     *  stored in the servlet context and shared by all instances of the engine
+     *  for the same application (within the same JVM; the global is
+     *  <em>not</em> shared between nodes in a cluster).
+     *
+     *  <p>Returns the global object, if it exists, or null if not defined.
+     *
+     *  @since 2.3
+     * 
+     **/
+
+    public Object getGlobal();
+
+    /**
+     *  Returns true if the application allows the reset service.
+     *
+     *  @since 0.2.9
+     * 
+     **/
+
+    public boolean isResetServiceEnabled();
+
+    /**
+     *  Returns a source for parsed 
+     *  {@link org.apache.tapestry.IScript}s.  The source is 
+     *  shared between all sessions.
+     *
+     *  @since 1.0.2
+     *
+     **/
+
+    public IScriptSource getScriptSource();
+
+    /**
+     *  Returns true if the engine has state and, therefore, should be stored
+     *  in the HttpSession.  This starts as false, but becomes true when
+     *  the engine requires state (such as when a visit object is created,
+     *  or when a peristent page property is set).
+     *
+     *  @since 1.0.2
+     *
+     **/
+
+    public boolean isStateful();
+
+	/**
+	 *  Returns a shared object that allows components to find
+	 *  their set of localized strings.
+	 * 
+	 *  @since 2.0.4
+	 * 
+     *  @see org.apache.tapestry.engine.AbstractEngine#createComponentStringsSource(RequestContext)
+     * 
+	 **/
+	
+	public IComponentMessagesSource getComponentMessagesSource();
+
+    /**
+     *  Returns a shared instance of {@link org.apache.tapestry.util.io.DataSqueezer}.
+     * 
+     *  @since 2.2
+     * 
+     *  @see org.apache.tapestry.engine.AbstractEngine#createDataSqueezer()
+     * 
+     **/
+    
+    public DataSqueezer getDataSqueezer();
+
+    /**
+     *  Returns a {@link org.apache.tapestry.engine.IPropertySource} that should be
+     *  used to obtain configuration data.  The returned source represents
+     *  a search path that includes (at a minimum):
+     *  
+     *  <ul>
+     *  <li>Properties of the {@link org.apache.tapestry.spec.ApplicationSpecification}
+     *  <li>Initial Parameters of servlet (configured in the <code>web.xml</code> deployment descriptor)
+     *  <li>Initial Parameter of the servlet context (also configured in <code>web.xml</code>)
+     *  <li>System properties (defined with the <code>-D</code> JVM command line parameter)
+     *  <li>Hard-coded "factory defaults" (for some properties)
+     *  </ul>
+     * 
+     *  @since 2.3
+     *  @see org.apache.tapestry.engine.AbstractEngine#createPropertySource(RequestContext)
+     * 
+     **/
+
+    public IPropertySource getPropertySource();
+    
+    /**
+     *  Returns a {@link org.apache.tapestry.util.pool.Pool} that is used
+     *  to store all manner of objects that are needed throughout the system.
+     *  This is the best way to deal with objects that are both expensive to
+     *  create and not threadsafe.  The reset service
+     *  will clear out this Pool.
+     * 
+     *  @since 3.0
+     *  @see org.apache.tapestry.engine.AbstractEngine#createPool(RequestContext)
+     * 
+     **/
+    
+    public Pool getPool();
+    
+    /**
+     *  Returns an object that can create enhanced versions of component classes.
+     * 
+     *  @since 3.0
+     *  @see org.apache.tapestry.engine.AbstractEngine#createComponentClassEnhancer(RequestContext)
+     * 
+     **/
+    
+    public IComponentClassEnhancer getComponentClassEnhancer();
+    
+    /**
+     *  Returns the encoding to be used to generate the servlet responses and 
+     *  accept the servlet requests.
+     * 
+     *  @since 3.0
+     * 
+     **/
+    
+    public String getOutputEncoding();
+    
+    /**
+     * Returns an object that can compute the checksum of a resource.
+     * @since 3.0.3
+     */
+    public ResourceChecksumSource getResourceChecksumSource();
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/IExternalPage.java b/tapestry-framework/src/org/apache/tapestry/IExternalPage.java
new file mode 100644
index 0000000..e306845
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/IExternalPage.java
@@ -0,0 +1,48 @@
+//  Copyright 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.tapestry;
+
+
+/**
+ *  Defines a page which may be referenced externally via a URL using the 
+ *  {@link org.apache.tapestry.engine.ExternalService}. External pages may be bookmarked 
+ *  via their URL for latter display. See the 
+ *  {@link org.apache.tapestry.link.ExternalLink} for details on how to invoke
+ *  <tt>IExternalPage</tt>s.
+ * 
+ *  @see org.apache.tapestry.callback.ExternalCallback
+ *  @see org.apache.tapestry.engine.ExternalService
+ *
+ *  @author Howard Lewis Ship
+ *  @author Malcolm Edgar
+ *  @version $Id$
+ *  @since 2.2
+ **/
+
+public interface IExternalPage extends IPage
+{
+    /**
+     *  Initialize the external page with the given array of parameters and
+     *  request cycle.
+     *  <p>
+     *  This method is invoked after {@link IPage#validate(IRequestCycle)}.
+     *
+     *  @param parameters the array of page parameters
+     *  @param cycle current request cycle
+     * 
+     **/
+
+    public void activateExternalPage(Object[] parameters, IRequestCycle cycle);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/IForm.java b/tapestry-framework/src/org/apache/tapestry/IForm.java
new file mode 100644
index 0000000..dabe67c
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/IForm.java
@@ -0,0 +1,174 @@
+//  Copyright 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.tapestry;
+
+import org.apache.tapestry.form.FormEventType;
+import org.apache.tapestry.form.IFormComponent;
+import org.apache.tapestry.valid.IValidationDelegate;
+
+/**
+ *  A generic way to access a component which defines an HTML form.  This interface
+ *  exists so that the {@link IRequestCycle} can invoke the
+ *  {@link #rewind(IMarkupWriter, IRequestCycle)} method (which is used to deal with
+ *  a Form that uses the direct service).  In release 1.0.5, more responsibility
+ *  for forms was moved here.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.2
+ **/
+
+public interface IForm extends IAction
+{
+
+    /**
+     *  Attribute name used with the request cycle; allows other components to locate
+     *  the IForm.
+     *
+     *  @since 1.0.5
+     * 
+     **/
+
+    public static final String ATTRIBUTE_NAME = "org.apache.tapestry.active.Form";
+
+    /**
+     *  Invoked by the {@link IRequestCycle} to allow a form that uses
+     *  the direct service, to respond to the form submission.
+     *
+     **/
+
+    public void rewind(IMarkupWriter writer, IRequestCycle cycle);
+
+    /**
+     *  Adds an additional event handler.  The type determines when the
+     *  handler will be invoked, {@link FormEventType#SUBMIT}
+     *  is most typical.
+     *
+     * @since 1.0.5
+     * 
+     **/
+
+    public void addEventHandler(FormEventType type, String functionName);
+
+    /**
+     *  Constructs a unique identifier (within the Form).  The identifier
+     *  consists of the component's id, with an index number added to
+     *  ensure uniqueness.
+     *
+     *  <p>Simply invokes {@link #getElementId(IFormComponent, String)} with the component's id.
+     *
+     *
+     *  @since 1.0.5
+     * 
+     **/
+
+    public String getElementId(IFormComponent component);
+
+    /**
+     *  Constructs a unique identifier from the base id.  If possible, the
+     *  id is used as-is.  Otherwise, a unique identifier is appended
+     *  to the id.
+     *
+     *  <p>This method is provided simply so that some components
+     *  ({@link org.apache.tapestry.form.ImageSubmit}) have more specific control over
+     *  their names.
+     * 
+     *  <p>Invokes {@link IFormComponent#setName(String)} with the result, as well
+     *  as returning it.
+     * 
+     *  @throws StaleLinkException if, when the form itself is rewinding, the
+     *  element id allocated does not match the expected id (as allocated when the form rendered).
+     *  This indicates that the state of the application has changed between the time the
+     *  form renderred and the time it was submitted.
+     *
+     *  @since 1.0.5
+     *
+     **/
+
+    public String getElementId(IFormComponent component, String baseId);
+
+    /**
+     * Returns the name of the form.
+     *
+     *  @since 1.0.5
+     *
+     **/
+
+    public String getName();
+
+    /**
+     *  Returns true if the form is rewinding (meaning, the form was the subject
+     *  of the request cycle).
+     *
+     *  @since 1.0.5
+     *
+     **/
+
+    public boolean isRewinding();
+
+    /**
+     *  Returns the validation delegate for the form.
+     *  Returns null if the form does not have a delegate.
+     * 
+     *  @since 1.0.8
+     * 
+     **/
+
+    public IValidationDelegate getDelegate();
+    
+    /**
+     *  May be invoked by a component to force the encoding type of the
+     *  form to a particular value.
+     * 
+     *  @see org.apache.tapestry.form.Upload
+     *  @throws ApplicationRuntimeException if the current encoding type is not null
+     *  and doesn't match the suggested encoding type
+     *  @since 3.0
+     * 
+     **/
+    
+    public void setEncodingType(String encodingType);
+    
+    
+    /**
+     * Adds a hidden field value to be stored in the form. This ensures that all
+     * of the &lt;input type="hidden"&gt; (or equivalent) are grouped together, 
+     * which ensures that the output HTML is valid (ie. doesn't 
+     * have &lt;input&gt; improperly nested with &lt;tr&gt;, etc.).
+     * 
+     * <p>
+     * It is acceptible to add multiple hidden fields with the same name.
+     * They will be written in the order they are received.
+     * 
+     * @since 3.0
+     */
+
+	public void addHiddenValue(String name, String value);
+
+	/**
+	 * Adds a hidden field value to be stored in the form. This ensures that all
+	 * of the &lt;input type="hidden"&gt; (or equivalent) are grouped together, 
+	 * which ensures that the output HTML is valid (ie. doesn't 
+	 * have &lt;input&gt; improperly nested with &lt;tr&gt;, etc.).
+	 * 
+	 * <p>
+	 * It is acceptible to add multiple hidden fields with the same name.
+	 * They will be written in the order they are received.
+	 * 
+	 * @since 3.0
+	 */
+
+	public void addHiddenValue(String name, String id, String value);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/ILocatable.java b/tapestry-framework/src/org/apache/tapestry/ILocatable.java
new file mode 100644
index 0000000..02752e1
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/ILocatable.java
@@ -0,0 +1,37 @@
+//  Copyright 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.tapestry;
+
+/**
+ *  Interface for classes that may be linked to a specific
+ *  {@link org.apache.tapestry.ILocation location}.  This
+ *  is primarily used when reporting errors.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public interface ILocatable
+{
+	/**
+	 *  Returns the {@link org.apache.tapestry.ILocation location} from which
+	 *  this object orginates, or null if not known.
+	 * 
+	 **/
+	
+	public ILocation getLocation();
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/ILocation.java b/tapestry-framework/src/org/apache/tapestry/ILocation.java
new file mode 100644
index 0000000..a3e9ddd
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/ILocation.java
@@ -0,0 +1,28 @@
+//  Copyright 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.tapestry;
+
+/**
+ *  TODO Add Type comment
+ * 
+ * @author glongman@intelligentworks.com
+ * @version $Id$
+ */
+public interface ILocation
+{
+    public abstract IResourceLocation getResourceLocation();
+    public abstract int getLineNumber();
+    public abstract int getColumnNumber();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/ILocationHolder.java b/tapestry-framework/src/org/apache/tapestry/ILocationHolder.java
new file mode 100644
index 0000000..3fd9fe1
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/ILocationHolder.java
@@ -0,0 +1,32 @@
+//  Copyright 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.tapestry;
+
+/**
+ *  Interface for objects that are
+ *  read from resource files, used to backtrace
+ *  live objects to the resources they
+ *  came from. 
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public interface ILocationHolder extends ILocatable
+{
+    public void setLocation(ILocation location);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/IMarkupWriter.java b/tapestry-framework/src/org/apache/tapestry/IMarkupWriter.java
new file mode 100644
index 0000000..5da6b66
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/IMarkupWriter.java
@@ -0,0 +1,253 @@
+//  Copyright 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.tapestry;
+
+/**
+ *  Defines an object that can write markup (XML, HTML, XHTML) style output.
+ *  A <code>IMarkupWriter</code> handles translation from unicode to
+ *  the markup language (escaping characters such as '&lt;' and '&gt;' to
+ *  their entity equivalents, '&amp;lt;' and '&amp;gt;') as well as assisting
+ *  with nested elements, closing tags, etc.
+ *
+ *  @author Howard Ship, David Solis
+ *  @version $Id$
+ **/
+
+public interface IMarkupWriter
+{
+    /**
+     * Writes an integer attribute into the currently open tag.
+     *
+     * @throws IllegalStateException if there is no open tag.
+     *
+     **/
+
+    public void attribute(String name, int value);
+
+    /**
+     * Writes a boolean attribute into the currently open tag.
+     *
+     * @throws IllegalStateException if there is no open tag.
+     *
+     * @since 3.0
+     **/
+
+    public void attribute(String name, boolean value);
+
+    /**
+     * Writes an attribute into the most recently opened tag. This must be called after
+     * {@link #begin(String)}
+     * and before any other kind of writing (which closes the tag).
+     *
+     * <p>The value may be null.
+     *
+     * @throws IllegalStateException if there is no open tag.
+     **/
+
+    public void attribute(String name, String value);
+
+    /**
+     * Similar to {@link #attribute(String, String)} but no escaping of invalid elements
+     * is done for the value.
+     * 
+     * @throws IllegalStateException if there is no open tag.
+     *
+     * @since 3.0
+     **/
+
+    public void attributeRaw(String name, String value);
+
+    /**
+     * Closes any existing tag then starts a new element. The new element is pushed
+     * onto the active element stack.
+     **/
+
+    public void begin(String name);
+
+    /**
+     * Starts an element that will not later be matched with an <code>end()</code>
+     * call. This is useful for elements that
+     * do not need closing tags.
+     *
+     **/
+
+    public void beginEmpty(String name);
+
+    /**
+     * Invokes checkError() on the <code>PrintWriter</code> used to
+     *  format output.
+     **/
+
+    public boolean checkError();
+
+    /**
+     * Closes this <code>IMarkupWriter</code>. Close tags are
+     * written for any active elements. The <code>PrintWriter</code>
+     * is then sent <code>close()</code>.  A nested writer will commit
+     * its buffer to its containing writer.
+     *
+     **/
+
+    public void close();
+
+    /**
+     * Closes the most recently opened element by writing the '&gt;' that ends
+     * it. Once this is invoked, the <code>attribute()</code> methods
+     * may not be used until a new element is opened with {@link #begin(String)} or
+     * or {@link #beginEmpty(String)}.
+     **/
+
+    public void closeTag();
+
+    /**
+     * Writes an XML/HTML comment. Any open tag is first closed. 
+     * The method takes care of
+     * providing the <code>&lt;!--</code> and <code>--&gt;</code>, and
+     * provides a blank line after the close of the comment.
+     *
+     * <p><em>Most</em> characters are valid inside a comment, so no check
+     * of the contents is made (much like {@link #printRaw(String)}.
+     *
+     **/
+
+    public void comment(String value);
+
+    /**
+     * Ends the element most recently started by {@link
+     * #begin(String)}.  The name of the tag is popped off of the
+     * active element stack and used to form an HTML close tag.
+     *
+     **/
+
+    public void end();
+
+    /**
+     * Ends the most recently started element with the given
+     * name. This will also end any other intermediate
+     * elements. This is very useful for easily ending a table or
+     * even an entire page.
+     *
+     **/
+
+    public void end(String name);
+
+    /**
+     * Forwards <code>flush()</code> to this 
+     * <code>IMarkupWriter</code>'s <code>PrintWriter</code>.
+     *
+     **/
+
+    public void flush();
+
+    /**
+     *  Returns a nested writer, one that accumulates its changes in a
+     *  buffer.  When the nested  writer is closed, it writes its
+     *  buffer into its containing <code>IMarkupWriter</code>.
+     *
+     **/
+
+    public IMarkupWriter getNestedWriter();
+
+    /**
+     *
+     * The primary <code>print()</code> method, used by most other
+     * methods.
+     *
+     * <p>Prints the character array, first closing any open tag. Problematic characters
+     * ('&lt;', '&gt;' and '&amp;') are converted to appropriate
+     * entities.
+     *
+     * <p>Does <em>nothing</em> if <code>data</code> is null.
+     *
+     * <p>Closes any open tag.
+     *
+     **/
+
+    public void print(char[] data, int offset, int length);
+
+    /**
+     * Prints a single character, or its equivalent entity.
+     *
+     * <p>Closes any open tag.
+     *
+     **/
+
+    public void print(char value);
+
+    /**
+     * Prints an integer.
+     *
+     * <p>Closes any open tag.
+     *
+     **/
+
+    public void print(int value);
+
+    /**
+     * Invokes {@link #print(char[], int, int)} to print the string.  Use
+     * {@link #printRaw(String)} if the character data is known to be safe.
+     *
+     * <p>Does <em>nothing</em> if <code>value</code> is null.
+     *
+     * <p>Closes any open tag.
+     *
+     * @see #print(char[], int, int)
+     *
+     **/
+
+    public void print(String value);
+
+    /**
+     * Closes the open tag (if any), then prints a line seperator to
+     * the output stream.
+     *
+     **/
+
+    public void println();
+
+    /**
+     * Prints a portion of an output buffer to the stream.
+     * No escaping of invalid elements is done, which
+     * makes this more effecient than <code>print()</code>. 
+     * Does <em>nothing</em> if <code>buffer</code>
+     * is null.
+     *
+     * <p>Closes any open tag.
+     *
+     **/
+
+    public void printRaw(char[] buffer, int offset, int length);
+
+    /**
+     * Prints output to the stream. No escaping of invalid elements is done, which
+     * makes this more effecient than <code>print()</code>.
+     *
+     * <p>Does <em>nothing</em> if <code>value</code>
+     * is null.
+     *
+     * <p>Closes any open tag.
+     *
+     **/
+
+    public void printRaw(String value);
+
+    /**
+     *  Returns the type of content generated by this response writer, as
+     *  a MIME type.
+     *
+     **/
+
+    public String getContentType();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/IMessages.java b/tapestry-framework/src/org/apache/tapestry/IMessages.java
new file mode 100644
index 0000000..f497754
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/IMessages.java
@@ -0,0 +1,98 @@
+//  Copyright 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.tapestry;
+
+/**
+ * A set of localized message strings.  This is somewhat like
+ * a {@link java.util.ResourceBundle}, but with more
+ * flexibility about where the messages come from.  In addition,
+ * it includes methods similar to {@link java.text.MessageFormat}
+ * for formatting the strings.
+ *
+ * @see org.apache.tapestry.IComponent#getMessages
+ * @see org.apache.tapestry.engine.IComponentMessagesSource
+ * 
+ * @author Howard Lewis Ship
+ * @since 2.0.4
+ *
+ */
+
+public interface IMessages
+{
+    /**
+     * Searches for a localized string with the given key.
+     * If not found, a modified version of the key
+     * is returned (all upper-case and surrounded by square
+     * brackets).
+     * 
+     */
+
+    public String getMessage(String key);
+
+    /**
+     * Searches for a localized string with the given key.
+     * If not found, then the default value (which should already
+     * be localized) is returned.  Passing a default of null
+     * is useful when trying to determine if the strings contains
+     * a given key.
+     *
+     */
+
+    public String getMessage(String key, String defaultValue);
+
+    /**
+     * Formats a string, using
+     * {@link java.text.MessageFormat#format(java.lang.String, java.lang.Object[])}.
+     * 
+     * <p>
+     * In addition, special processing occurs for any of the arguments that
+     * inherit from {@link Throwable}: such arguments are replaced with the Throwable's message
+     * (if non blank), or the Throwable's class name (if the message is blank).
+     *
+     * @param key the key used to obtain a localized pattern using
+     * {@link #getMessage(String)}
+     * @param arguments passed to the formatter
+     *
+     * @since 3.0
+     *
+     */
+
+    public String format(String key, Object[] arguments);
+
+    /**
+     * Convienience method for invoking {@link #format(String, Object[])}.
+     * @since 3.0
+     *
+     */
+    public String format(String key, Object argument);
+
+    /**
+     * Convienience method for invoking {@link #format(String, Object[])}.
+     *
+     * @since 3.0
+     * 
+     */
+
+    public String format(String key, Object argument1, Object argument2);
+
+    /**
+     * Convienience method for invoking {@link #format(String, Object[])}.
+     *
+     * @since 3.0
+     * 
+     */
+
+    public String format(String key, Object argument1, Object argument2, Object argument3);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/INamespace.java b/tapestry-framework/src/org/apache/tapestry/INamespace.java
new file mode 100644
index 0000000..6d9f13f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/INamespace.java
@@ -0,0 +1,270 @@
+//  Copyright 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.tapestry;
+
+import java.util.List;
+
+import org.apache.tapestry.spec.IComponentSpecification;
+import org.apache.tapestry.spec.ILibrarySpecification;
+
+/**
+ *  Organizes different libraries of Tapestry pages, components
+ *  and services into "frameworks", used to disambiguate names.
+ * 
+ *  <p>
+ *  Tapestry release 3.0 includes dynamic discovery of pages and components; an application
+ *  or library may contain a page or component that won't be "known" until the name
+ *  is resolved (because it involves searching for a particular named file).
+ * 
+ *  @see org.apache.tapestry.resolver.PageSpecificationResolver
+ *  @see org.apache.tapestry.resolver.ComponentSpecificationResolver
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.2
+ *
+ **/
+
+public interface INamespace extends ILocatable
+{
+    /**
+     *  Reserved name of a the implicit Framework library.
+     * 
+     **/
+
+    public static final String FRAMEWORK_NAMESPACE = "framework";
+
+    /**
+     *  Character used to seperate the namespace prefix from the page name
+     *  or component type.
+     * 
+     *  @since 2.3
+     * 
+     **/
+
+    public static final char SEPARATOR = ':';
+
+    /**
+     *  Returns an identifier for the namespace.  Identifiers
+     *  are simple names (they start with a letter,
+     *  and may contain letters, numbers, underscores and dashes).
+     *  An identifier must be unique among a namespaces siblings.
+     * 
+     *  <p>The application namespace has a null id; the framework
+     *  namespace has an id of "framework".
+     * 
+     **/
+
+    public String getId();
+
+    /**
+     *  Returns the extended id for this namespace, which is
+     *  a dot-seperated sequence of ids.
+     * 
+     **/
+
+    public String getExtendedId();
+
+    /**
+     *  Returns a version of the extended id appropriate for error
+     *  messages.  This is the based on
+     *  {@link #getExtendedId()}, unless this is the
+     *  application or framework namespace, in which case
+     *  special strings are returned.
+     *  
+     *  @since 3.0
+     * 
+     **/
+
+    public String getNamespaceId();
+
+    /**
+     *  Returns the parent namespace; the namespace which
+     *  contains this namespace.
+     * 
+     *  <p>
+     *  The application and framework namespaces return null
+     *  as the parent.
+     * 
+     **/
+
+    public INamespace getParentNamespace();
+
+    /**
+     *  Returns a namespace contained by this namespace.
+     * 
+     *  @param id either a simple name (of a directly contained namespace),
+     *  or a dot-seperarated name sequence.
+     *  @return the child namespace
+     *  @throws ApplicationRuntimeException if no such namespace exist.
+     * 
+     **/
+
+    public INamespace getChildNamespace(String id);
+
+    /**
+     *  Returns a sorted, immutable list of the ids of the immediate
+     *  children of this namespace.  May return the empty list,
+     *  but won't return null.
+     * 
+     **/
+
+    public List getChildIds();
+
+    /**
+     *  Returns the page specification of the named
+     *  page (defined within the namespace).
+     * 
+     *  @param name the name of the page
+     *  @return the specification
+     *  @throws ApplicationRuntimeException if the page specification
+     *  doesn't exist or can't be loaded
+     * 
+     **/
+
+    public IComponentSpecification getPageSpecification(String name);
+
+    /**
+     *  Returns true if this namespace contains the specified
+     *  page name.
+     * 
+     **/
+
+    public boolean containsPage(String name);
+
+    /**
+     *  Returns a sorted list of page names.  May return an empty
+     *  list, but won't return null.  The return list is immutable.
+     * 
+     **/
+
+    public List getPageNames();
+
+    /**
+     *  Returns the path for the named component (within the namespace).
+     * 
+     *  @param type the component alias
+     *  @return the specification path of the component
+     *  @throws ApplicationRuntimeException if the specification
+     *  doesn't exist or can't be loaded
+     * 
+     **/
+
+    public IComponentSpecification getComponentSpecification(String type);
+
+    /**
+     *  Returns true if the namespace contains the indicated component type.
+     * 
+     *  @param type a simple component type (no namespace prefix is allowed)
+     *
+     **/
+
+    public boolean containsComponentType(String type);
+
+    /**
+     *  Returns a sorted list of component types.  May return 
+     *  an empty list, but won't return null.  The return list
+     *  is immutable.  Represents just the known component types
+     *  (additional types may be discoverred dynamically).
+     * 
+     *  <p>Is this method even needed?
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    public List getComponentTypes();
+
+    /**
+     *  Returns the class name of a service provided by the
+     *  namespace.
+     * 
+     *  @param name the name of the service.
+     *  @return the complete class name of the service, or null
+     *  if the namespace does not contain the named service.
+     * 
+     **/
+
+    public String getServiceClassName(String name);
+
+    /**
+     *  Returns the names of all services provided by the
+     *  namespace, as a sorted, immutable list.  May return
+     *  the empty list, but won't return null.
+     * 
+     **/
+
+    public List getServiceNames();
+
+    /**
+     *  Returns the {@link org.apache.tapestry.spec.LibrarySpecification} from which
+     *  this namespace was created.
+     * 
+     **/
+
+    public ILibrarySpecification getSpecification();
+
+    /**
+     *  Constructs a qualified name for the given simple page name by
+     *  applying the correct prefix (if any).
+     * 
+     *  @since 2.3
+     * 
+     **/
+
+    public String constructQualifiedName(String pageName);
+
+    /**
+     *  Returns the location of the resource from which the
+     *  specification for this namespace was read.
+     * 
+     **/
+
+    public IResourceLocation getSpecificationLocation();
+
+    /**
+     *  Returns true if the namespace is the special
+     *  application namespace (which has special search rules
+     *  for handling undeclared pages and components).
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    public boolean isApplicationNamespace();
+
+    /**
+     *  Used to specify additional pages beyond those that came from
+     *  the namespace's specification.  This is used when pages
+     *  in the application namespace are dynamically discovered.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    public void installPageSpecification(String pageName, IComponentSpecification specification);
+
+    /**
+     *  Used to specify additional components beyond those that came from
+     *  the namespace's specification.  This is used when components
+     *  in the application namespace are dynamically discovered.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    public void installComponentSpecification(String type, IComponentSpecification specification);
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/IPage.java b/tapestry-framework/src/org/apache/tapestry/IPage.java
new file mode 100644
index 0000000..6470f17
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/IPage.java
@@ -0,0 +1,312 @@
+//  Copyright 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.tapestry;
+
+import java.io.OutputStream;
+import java.util.Locale;
+
+import org.apache.tapestry.event.ChangeObserver;
+import org.apache.tapestry.event.PageDetachListener;
+import org.apache.tapestry.event.PageRenderListener;
+import org.apache.tapestry.event.PageValidateListener;
+
+/**
+ *  A root level component responsible for generating an entire a page
+ *  within the application.
+ *
+ *  <p>Pages are created dynamically from thier class names (part of the
+ *  {@link org.apache.tapestry.spec.IComponentSpecification}).
+ *
+ *  @see org.apache.tapestry.engine.IPageSource
+ *  @see org.apache.tapestry.engine.IPageLoader
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public interface IPage extends IComponent
+{
+    /**
+     *  Invoked on a page when it is no longer needed by
+     *  the engine, just before is is
+     *  returned to the pool.  The page is expected to
+     *  null the engine, visit and changeObserver properties.
+     * 
+     *  <p>Classes should also reset any properties to
+     *  default values (as if the instance
+     *  was freshly instantiated).
+     *
+     *  @see org.apache.tapestry.engine.IPageSource#releasePage(IPage)
+     *
+     **/
+
+    public void detach();
+
+    /**
+     *  Returns the {@link IEngine} that the page is currently
+     *  attached to.
+     *
+     **/
+
+    public IEngine getEngine();
+
+    /**
+     *  Returns the object (effectively, an 
+     *  {@link org.apache.tapestry.engine.IPageRecorder}) that is notified
+     *  of any changes to persistant properties of the page.
+     *
+     **/
+
+    public ChangeObserver getChangeObserver();
+
+    /**
+     *  Returns the <code>Locale</code> of the page.
+     *  The locale may be used to determine what template is
+     *  used by the page and the components contained by the page.
+     *
+     **/
+
+    public Locale getLocale();
+
+    /**
+     *  Updates the page's locale.  This is write-once, a subsequent attempt
+     *  will throw an {@link ApplicationRuntimeException}.
+     *
+     **/
+
+    public void setLocale(Locale value);
+
+    /**
+     *  Returns the fully qualified name of the page, including its
+     *  namespace prefix, if any.
+     * 
+     *  @since 2.3
+     * 
+     **/
+
+    public String getPageName();
+
+    /**
+     *  Sets the name of the page.
+     * 
+     *  @param pageName fully qualified page name (including namespace prefix, if any)
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    public void setPageName(String pageName);
+
+    /**
+     *  Returns a particular component from within the page.  The path is a dotted
+     *  name sequence identifying the component.  It may be null
+     *  in which case the page returns itself.
+     *
+     *  @exception ApplicationRuntimeException runtime exception
+     *  thrown if the path does not identify a component.
+     *
+     **/
+
+    public IComponent getNestedComponent(String path);
+
+    /**
+     *  Attaches the page to the {@link IEngine engine}.
+     *  This method is used when a pooled page is
+     *  claimed for use with a particular engine; it will stay attached
+     *  to the engine until the end of the current request cycle,
+     *  then be returned to the pool.
+     * 
+     *  <p>This method is rarely overriden; to initialize
+     *  page properties before a render, override
+     *  {@link #beginResponse(IMarkupWriter, IRequestCycle)}.
+     *
+     **/
+
+    public void attach(IEngine value);
+
+    /**
+     *  Invoked to render the entire page.  This should only be invoked by
+     *  {@link IRequestCycle#renderPage(IMarkupWriter writer)}.
+     *
+     *  <p>The page performs a render using the following steps:
+     *
+     * <ul>
+     *  <li>Invokes {@link PageRenderListener#pageBeginRender(org.apache.tapestry.event.PageEvent)}
+     *  <li>Invokes {@link #beginResponse(IMarkupWriter, IRequestCycle)}
+     *  <li>Invokes {@link IRequestCycle#commitPageChanges()} (if not rewinding)
+     *  <li>Invokes {@link #render(IMarkupWriter, IRequestCycle)}
+     *  <li>Invokes {@link PageRenderListener#pageEndRender(org.apache.tapestry.event.PageEvent)} (this occurs
+     *  even if a previous step throws an exception).
+     * </ul>
+     *
+     **/
+
+    public void renderPage(IMarkupWriter writer, IRequestCycle cycle);
+
+    /**
+     *  Invoked before a partial render of the page occurs
+     *  (this happens when rewinding a {@link org.apache.tapestry.form.Form}
+     *  within the page).  The page is expected to fire appopriate
+     *  events.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public void beginPageRender();
+
+    /**
+     *  Invoked after a partial render of the page occurs
+     *  (this happens when rewinding a {@link org.apache.tapestry.form.Form}
+     *  within the page).  The page is expected to fire
+     *  appropriate events.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public void endPageRender();
+
+    public void setChangeObserver(ChangeObserver value);
+
+    /**
+     *  Method invoked by the page, action and direct services 
+     *  to validate that the
+     *  user is allowed to visit the page.
+     *
+     *  <p>Most web applications have a concept of 'logging in' and
+     *  pages that an anonymous (not logged in) user should not be
+     *  able to visit directly.  This method acts as the first line of
+     *  defense against a malicous user hacking URLs.
+     *
+     *  <p>Pages that should be protected will typically throw a {@link
+     *  PageRedirectException}, to redirect the user to an appropriate
+     *  part of the system (such as, a login page).
+     * 
+     *  <p>Since 3.0, it is easiest to not override this method,
+     *  but to implement the {@link PageValidateListener} interface
+     *  instead.
+     *
+     **/
+
+    public void validate(IRequestCycle cycle);
+
+    /**
+     *  Invoked to create a response writer appropriate to the page
+     *  (i.e., appropriate to the content of the page).
+     *
+     **/
+
+    public IMarkupWriter getResponseWriter(OutputStream out);
+
+    /**
+     *  Invoked just before rendering of the page is initiated.  This gives
+     *  the page a chance to perform any additional setup.  One possible behavior is
+     *  to set HTTP headers and cookies before any output is generated.
+     *
+     *  <p>The timing of this explicitly <em>before</em> {@link org.apache.tapestry.engine.IPageRecorder page recorder}
+     *  changes are committed.  Rendering occurs <em>after</em> the recorders
+     *  are committed, when it is too late to make changes to dynamic page
+     *  properties.
+     *
+     *
+     **/
+
+    public void beginResponse(IMarkupWriter writer, IRequestCycle cycle);
+
+    /**
+     *  Returns the current {@link IRequestCycle}.  This is set when the
+     *  page is loaded (or obtained from the pool) and attached to the
+     *  {@link IEngine engine}.
+     *
+     **/
+
+    public IRequestCycle getRequestCycle();
+
+    /**
+     *  Invoked by the {@link IRequestCycle} to inform the page of the cycle,
+     *  as it is loaded.
+     *
+     **/
+
+    public void setRequestCycle(IRequestCycle cycle);
+
+    /**
+     *  Returns the visit object for the application; the visit object
+     *  contains application-specific information.
+     *
+     **/
+
+    public Object getVisit();
+
+    /**
+     *  Returns the globally shared application object. The global object is
+     *  stored in the servlet context.
+     *
+     *  <p>Returns the global object, if it exists, or null if not defined.
+     *
+     *  @since 2.3
+     * 
+     **/
+
+    public Object getGlobal();
+
+    /**
+     *  @since 1.0.5
+     *
+     **/
+
+    public void addPageRenderListener(PageRenderListener listener);
+
+    /**
+     *
+     *  @since 2.1
+     * 
+     **/
+
+    public void removePageRenderListener(PageRenderListener listener);
+
+    /**
+     *  @since 1.0.5
+     *
+     **/
+
+    public void addPageDetachListener(PageDetachListener listener);
+
+    /**
+     * 
+     *  @since 2.1
+     * 
+     **/
+
+    public void removePageDetachListener(PageDetachListener listener);
+
+    /**
+     *  @since 3.0
+     *
+     **/
+
+    public void addPageValidateListener(PageValidateListener listener);
+
+    /**
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    public void removePageValidateListener(PageValidateListener listener);
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/IRender.java b/tapestry-framework/src/org/apache/tapestry/IRender.java
new file mode 100644
index 0000000..e4bfc90
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/IRender.java
@@ -0,0 +1,45 @@
+//  Copyright 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.tapestry;
+
+/**
+ *  An element that may be asked to render itself to an
+ *  {@link IMarkupWriter} using a {@link IRequestCycle}.
+ *
+ *  <p>This primarily includes {@link IComponent} and {@link IPage},
+ *  but also extends to other things, such as objects responsible for
+ *  rendering static markup text.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+public interface IRender
+{
+    /**
+     *  The principal rendering/rewinding method.  This will cause
+     *  the receiving component to render its top level elements (HTML
+     *  text and components).
+     *
+     *  <p>Renderring and rewinding are the exact same process.  The
+     *  same code that renders must be able to restore state by going
+     *  through the exact same operations (even though the output is
+     *  discarded).
+     *
+     **/
+
+    public void render(IMarkupWriter writer, IRequestCycle cycle);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/IRequestCycle.java b/tapestry-framework/src/org/apache/tapestry/IRequestCycle.java
new file mode 100644
index 0000000..23d555e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/IRequestCycle.java
@@ -0,0 +1,313 @@
+//  Copyright 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.tapestry;
+
+import org.apache.tapestry.engine.IEngineService;
+import org.apache.tapestry.engine.IMonitor;
+import org.apache.tapestry.request.RequestContext;
+
+/**
+ *  Controller object that manages a single request cycle.  A request cycle
+ *  is one 'hit' on the web server.  In the case of a Tapestry application,
+ *  this will involve:
+ *  <ul>
+ *  <li>Responding to the URL by finding an {@link IEngineService} object
+ *  <li>Determining the result page
+ *  <li>Renderring the result page
+ *  <li>Releasing any resources
+ *  </ul>
+ *
+ *  <p>Mixed in with this is:
+ *  <ul>
+ *  <li>Exception handling
+ *  <li>Loading of pages and templates from resources
+ *  <li>Tracking changes to page properties, and restoring pages to prior states
+ *  <li>Pooling of page objects
+ *  </ul>
+ *
+ *  <p>A request cycle is broken up into two phases.   The <em>rewind</em> phase
+ *  is optional, as it tied to {@link org.apache.tapestry.link.ActionLink} or
+ *  {@link org.apache.tapestry.form.Form} components.  In the rewind phase,
+ *  a previous page render is redone (discarding output) until a specific component
+ *  of the page is reached.  This rewinding ensures that the page
+ *  is restored to the exact state it had when the URL for the request cycle was
+ *  generated, taking into account the dynamic nature of the page ({@link org.apache.tapestry.components.Foreach},
+ *  {@link org.apache.tapestry.components.Conditional}, etc.).  Once this component is reached, it can notify
+ *  its {@link IActionListener}.  The listener has the ability to update the state
+ *  of any pages and select a new result page.
+ *
+ *  <p>Following the rewind phase is the <em>render</em> phase.  During the render phase,
+ *  a page is actually rendered and output sent to the client web browser.
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public interface IRequestCycle
+{
+    /**
+     *  Invoked after the request cycle is no longer needed, to release any resources
+     *  it may have.  This includes releasing any loaded pages back to the page source.
+     *
+     **/
+
+    public void cleanup();
+
+    /**
+     *  Passes the String through 
+     *  {@link javax.servlet.http.HttpServletResponse#encodeURL(java.lang.String)}, which
+     *  ensures that the session id is encoded in the URL (if necessary).
+     *
+     **/
+
+    public String encodeURL(String URL);
+
+    /**
+     *  Returns the engine which is processing this request cycle.
+     *
+     **/
+
+    public IEngine getEngine();
+
+    /**
+     *  Retrieves a previously stored attribute, returning null
+     *  if not found.  Attributes allow components to locate each other; primarily
+     *  they allow a wrapped component to locate a component which wraps it.
+     *
+     **/
+
+    public Object getAttribute(String name);
+
+    public IMonitor getMonitor();
+
+    /**
+     *  Returns the next action id.  ActionLink ids are used to identify different actions on a
+     *  page (URLs that are related to dynamic page state).  
+     *
+     **/
+
+    public String getNextActionId();
+
+    /**
+     *  Identifies the active page, the page which will ultimately render the response.
+     *
+     **/
+
+    public IPage getPage();
+
+    /**
+     *  Returns the page with the given name.  If the page has been
+     *  previously loaded in the current request cycle, that page is
+     *  returned.  Otherwise, the engine's page loader is used to
+     *  load the page.
+     *
+     *  @see IEngine#getPageSource()
+     **/
+
+    public IPage getPage(String name);
+
+    public RequestContext getRequestContext();
+
+    /**
+     *  Returns true if the context is being used to rewind a prior
+     *  state of the page.  This is only true when there is a target
+     *  action id.
+     *
+     **/
+
+    public boolean isRewinding();
+
+    /**
+     *  Checks to see if the current action id matches the target
+     *  action id.  Returns true only if they match.  Returns false if
+     *  there is no target action id (that is, during page rendering).
+     *
+     *  <p>If theres a match on action id, then the component
+     *  is compared against the target component.  If there's a mismatch
+     *  then a {@link StaleLinkException} is thrown.
+     **/
+
+    public boolean isRewound(IComponent component) throws StaleLinkException;
+
+    /**
+     *  Removes a previously stored attribute, if one with the given name exists.
+     *
+     **/
+
+    public void removeAttribute(String name);
+
+    /**
+     *  Renders the given page.  Applications should always use this
+     *  method to render the page, rather than directly invoking
+     *  {@link IPage#render(IMarkupWriter, IRequestCycle)} since the
+     *  request cycle must perform some setup before rendering.
+     *
+     **/
+
+    public void renderPage(IMarkupWriter writer);
+
+    /**
+     *  Rewinds a page and executes some form of action when the
+     *  component with the specified action id is reached.
+     *
+     *  @see IAction
+     *
+     **/
+
+    public void rewindPage(String targetActionId, IComponent targetComponent);
+
+    /**
+     *  Allows a temporary object to be stored in the request cycle,
+     *  which allows otherwise unrelated objects to communicate.  This
+     *  is similar to <code>HttpServletRequest.setAttribute()</code>,
+     *  except that values can be changed and removed as well.
+     *
+     *  <p>This is used by components to locate each other.  A component, such
+     *  as {@link org.apache.tapestry.html.Body}, will write itself under a well-known name
+     *  into the request cycle, and components it wraps can locate it by that name.
+     **/
+
+    public void setAttribute(String name, Object value);
+
+    /**
+     *  Sets the page to be rendered.  This is called by a component
+     *  during the rewind phase to specify an alternate page to render
+     *  during the response phase.
+     * 
+     *  @deprecated To be removed in 3.1.  Use {@link #activate(IPage)}.
+     *
+     **/
+
+    public void setPage(IPage page);
+
+    /**
+     *  Sets the page to be rendered.  This is called by a component
+     *  during the rewind phase to specify an alternate page to render
+     *  during the response phase.
+     *
+     * @deprecated To be removed in 3.1.  Use {@link #activate(String)}.
+     * 
+     **/
+
+    public void setPage(String name);
+
+    /**
+     *  Invoked just before rendering the response page to get all
+     *  {@link org.apache.tapestry.engine.IPageRecorder page recorders} touched in this request cycle
+     *  to commit their changes (save them to persistant storage).
+     *
+     *  @see org.apache.tapestry.engine.IPageRecorder#commit()
+     **/
+
+    public void commitPageChanges();
+
+    /**
+     *  Returns the service which initiated this request cycle.  This may return
+     *  null (very early during the request cycle) if the service has not
+     *  yet been determined.
+     *
+     *  @since 1.0.1
+     **/
+
+    public IEngineService getService();
+
+    /**
+     *  Used by {@link IForm forms} to perform a <em>partial</em> rewind
+     *  so as to respond to the form submission (using the direct service).
+     *
+     *  @since 1.0.2
+     **/
+
+    public void rewindForm(IForm form, String targetActionId);
+
+    /**
+     *  Much like {@link IEngine#forgetPage(String)}, but the page stays active and can even
+     *  record changes, until the end of the request cycle, at which point it is discarded
+     *  (and any recorded changes are lost).
+     *  This is used in certain rare cases where a page has persistent state but is
+     *  being renderred "for the last time".
+     * 
+     *  @since 2.0.2
+     * 
+     **/
+
+    public void discardPage(String name);
+
+    /**
+     *  Invoked by a {@link IEngineService service} to store an array of application-specific parameters.
+     *  These can later be retrieved (typically, by an application-specific listener method)
+     *  by invoking {@link #getServiceParameters()}.
+     * 
+     *  <p>Through release 2.1, parameters was of type String[].  This is
+     *  an incompatible change in 2.2.
+     * 
+     *  @see org.apache.tapestry.engine.DirectService
+     *  @since 2.0.3
+     * 
+     **/
+
+    public void setServiceParameters(Object[] parameters);
+
+    /**
+     *  Returns parameters previously stored by {@link #setServiceParameters(Object[])}.
+     * 
+     *  <p>
+     *  Through release 2.1, the return type was String[].  This is
+     *  an incompatible change in 2.2.
+     * 
+     *  @since 2.0.3
+     * 
+     **/
+
+    public Object[] getServiceParameters();
+
+    /**
+     *  A convienience for invoking {@link #activate(IPage)}.  Invokes
+     *  {@link #getPage(String)} to get an instance of the named page.
+     *
+     *  @since 3.0
+     *  
+     **/
+
+    public void activate(String name);
+
+    /**
+     *  Sets the active page for the request.  The active page is the page
+     *  which will ultimately render the response.  The activate page
+     *  is typically set by the {@link IEngineService service}.  Frequently,
+     *  the active page is changed (from a listener method) to choose 
+     *  an alternate page to render the response).
+     * 
+     *  <p>
+     *  {@link IPage#validate(IRequestCycle)} is invoked on the
+     *  page to be activated.  {@link PageRedirectException} is caught
+     *  and the page specified in the exception will be the
+     *  active page instead (that is, a page may "pass the baton" to another
+     *  page using the exception).  The new page is also validated.  This
+     *  continues until a page does not throw {@link PageRedirectException}.
+     *  
+     *  <p>
+     *  Validation loops can occur, where page A redirects to page B and then page B
+     *  redirects back to page A (possibly with intermediate steps).  This is detected and results
+     *  in an {@link ApplicationRuntimeException}.
+     * 
+     *  @since 3.0
+     *  
+     */
+    public void activate(IPage page);
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/IResourceLocation.java b/tapestry-framework/src/org/apache/tapestry/IResourceLocation.java
new file mode 100644
index 0000000..933e597
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/IResourceLocation.java
@@ -0,0 +1,114 @@
+//  Copyright 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.tapestry;
+
+import java.net.URL;
+import java.util.Locale;
+
+/**
+ *  Describes the location of a resource, such as a specification
+ *  or template.  Resources may be located within the classpath,
+ *  or within the Web context root or somewhere else entirely.
+ * 
+ *  <p>
+ *  Resources may be either base or localized.  A localized
+ *  version of a base resource may be obtained
+ *  via {@link #getLocalization(Locale)}.
+ * 
+ *  <p>
+ *  Resource locations are used as Map keys, they must 
+ *  implement {@link java.lang.Object#hashCode()} and
+ *  {@link java.lang.Object#equals(java.lang.Object)}
+ *  properly.
+ * 
+ *  <p>
+ *  Resource locations are valid even if the corresponding
+ *  resource <i>doesn't exist</i>.  To verify if a localization
+ *  actually exists, use {@link #getResourceURL()}, which returns
+ *  null if the resource doesn't exist.  {@link #getLocalization(Locale)}
+ *  returns only real resource locations, where the resource exists.
+ * 
+ *  <p>
+ *  Folders must be represented with a trailing slash.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public interface IResourceLocation
+{
+    /**
+     *  Returns a URL for the resource.
+     * 
+     *  @return the URL for the resource if it exists, or null if it does not
+     * 
+     **/
+    
+    public URL getResourceURL();
+    
+    /**
+     *  Returns the file name portion of the resource location.
+     * 
+     **/
+    
+    public String getName();
+    
+    /**
+     *  Returns a localized version of this resource (or this resource, if no
+     *  appropriate localization is found).  Should only be invoked
+     *  on a base resource.
+     * 
+     *  @param locale to localize for, or null for no localization.
+     *  @return a localized version of this resource, of null if the resource
+     *  itself does not exist.
+     * 
+     **/
+    
+    public IResourceLocation getLocalization(Locale locale);
+    
+    /**
+     *  Returns at a relative location to this resource.  
+     *  The new resource may or may not exist; this can be determined
+     *  via {@link #getResourceURL()}.
+     * 
+     *  @param name name of new resource, possibly as a relative path, or
+     *  as an absolute path (starting with a slash).
+     * 
+     **/
+    
+    public IResourceLocation getRelativeLocation(String name);
+    
+    /**
+     *  Returns the path that represents the resource.  This should 
+     *  only be used when the type of resource is known.
+     * 
+     **/
+    
+    public String getPath();
+
+    /**
+     *  Returns the locale for which this resource has been localized 
+     *  or null if the resource has not been localized. This should 
+     *  only be used when the type of resource is known.
+     * 
+     *  This locale is the same or more general than the locale for which localization 
+     *  was requested. For example, if the requested locale was en_US, but only the file 
+     *  Home_en was found, this locale returned would be en. 
+     **/
+    
+    public Locale getLocale();
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/IResourceResolver.java b/tapestry-framework/src/org/apache/tapestry/IResourceResolver.java
new file mode 100644
index 0000000..d83717a
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/IResourceResolver.java
@@ -0,0 +1,67 @@
+//  Copyright 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.tapestry;
+
+import java.net.URL;
+
+import ognl.ClassResolver;
+
+/**
+ * An object which is used to resolve classes and class-path resources.
+ * This is needed because, in an application server, different class loaders
+ * will be loading the Tapestry framework and the specific Tapestry application.
+ *
+ * <p>The class loader for the framework needs to be able to see resources in
+ * the application, but the application's class loader is a descendent of the
+ * framework's class loader.  To resolve this, we need a 'hook', an instance
+ * that provides access to the application's class loader.
+ * 
+ * <p>To more easily support OGNL, this interface now extends
+ *  {@link ognl.ClassResolver}.
+ * 
+ * @author Howard Lewis Ship
+ * @version $Id$
+ **/
+
+public interface IResourceResolver extends ClassResolver
+{
+    /**
+     *  Forwarded, unchanged, to the class loader.  Returns null if the
+     *  resource is not found.
+     *
+     **/
+
+    public URL getResource(String name);
+
+    /**
+     *  Forwarded, to the the method
+     *  <code>Class.forName(String, boolean, ClassLoader)</code>, using
+     *  the application's class loader.
+     *
+     *  Throws an {@link ApplicationRuntimeException} on any error.
+     **/
+
+    public Class findClass(String name);
+    
+    /**
+     *  Returns a {@link java.lang.ClassLoader} that can see
+     *  all the classes the resolver can access.
+     * 
+     *  @since 3.0
+     * 
+     **/
+    
+    public ClassLoader getClassLoader();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/IScript.java b/tapestry-framework/src/org/apache/tapestry/IScript.java
new file mode 100644
index 0000000..3dddea5
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/IScript.java
@@ -0,0 +1,55 @@
+//  Copyright 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.tapestry;
+
+import java.util.Map;
+
+/**
+ *  An object that can convert a set of symbols into a collection of JavaScript statements.
+ *
+ *  <p>IScript implementation must be threadsafe.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.2
+ * 
+ **/
+
+public interface IScript
+{
+    /**
+     *  Returns the location from which the script was loaded.
+     *
+     **/
+
+    public IResourceLocation getScriptLocation();
+
+    /**
+     * Executes the script, which will read and modify the symbols {@link Map}.  The
+     * script works with the {@link IScriptProcessor} to get the generated JavaScript
+     * included on the page.
+     * 
+     * @param cycle the current request cycle
+     * @param processor an object that processes the results of the script, typically
+     * an instance of {@link org.apache.tapestry.html.Body}
+     * @param symbols Map of input symbols; execution of the script may modify the map,
+     * creating new output symbols
+     * 
+     * @see org.apache.tapestry.html.Body#get(IRequestCycle)
+     *
+     */
+
+    public void execute(IRequestCycle cycle, IScriptProcessor processor, Map symbols);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/IScriptProcessor.java b/tapestry-framework/src/org/apache/tapestry/IScriptProcessor.java
new file mode 100644
index 0000000..ec5179c
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/IScriptProcessor.java
@@ -0,0 +1,59 @@
+//  Copyright 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.tapestry;
+
+/**
+ * Defines methods needed by a {@link org.apache.tapestry.IScript} to
+ * execute.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * @since 3.0
+ * @see org.apache.tapestry.html.Body
+ */
+
+public interface IScriptProcessor
+{
+	/**
+	 * Adds scripting code to the main body.  During the render, multiple scripts may
+	 * render multiple bodies; all are concatinated together to form
+	 * a single block.
+	 */
+	
+	public void addBodyScript(String script);
+	
+	/**
+	 * Adds initialization script.  Initialization script is executed once, when
+	 * the containing page loads.  Effectively, this means that initialization script
+	 * is stored inside the HTML &lt;body&gt; element's <code>onload</code>
+	 * event handler.
+	 */
+	public void addInitializationScript(String script);
+	
+	/**
+	 * Adds an external script.  The processor is expected to ensure
+	 * that external scripts are only loaded a single time per page.
+	 */
+	
+	public void addExternalScript(IResourceLocation location);
+	
+	/**
+	 * Ensures that the given string is unique.  The string
+	 * is either returned unchanged, or a suffix is appended to
+	 * ensure uniqueness.
+	 */
+	
+	public String getUniqueString(String baseValue);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/Location.java b/tapestry-framework/src/org/apache/tapestry/Location.java
new file mode 100644
index 0000000..cd71e84
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/Location.java
@@ -0,0 +1,114 @@
+//  Copyright 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.tapestry;
+
+import org.apache.commons.lang.builder.EqualsBuilder;
+import org.apache.commons.lang.builder.HashCodeBuilder;
+
+/**
+ *  Implementation of the {@link org.apache.tapestry.ILocation} interface.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+public class Location implements ILocation
+{
+    private IResourceLocation _resourceLocation;
+    private int _lineNumber = -1;
+    private int _columnNumber = -1;
+
+    public Location(IResourceLocation location)
+    {
+        _resourceLocation = location;
+    }
+
+    public Location(IResourceLocation location, int lineNumber)
+    {
+        this(location);
+
+        _lineNumber = lineNumber;
+    }
+
+    public Location(IResourceLocation location, int lineNumber, int columnNumber)
+    {
+        this(location);
+
+        _lineNumber = lineNumber;
+        _columnNumber = columnNumber;
+    }
+
+    public IResourceLocation getResourceLocation()
+    {
+        return _resourceLocation;
+    }
+
+    public int getLineNumber()
+    {
+        return _lineNumber;
+    }
+
+    public int getColumnNumber()
+    {
+        return _columnNumber;
+    }
+
+    public int hashCode()
+    {
+        HashCodeBuilder builder = new HashCodeBuilder(237, 53);
+
+        builder.append(_resourceLocation);
+        builder.append(_lineNumber);
+        builder.append(_columnNumber);
+
+        return builder.toHashCode();
+    }
+
+    public boolean equals(Object other)
+    {
+        if (!(other instanceof ILocation))
+            return false;
+
+        ILocation l = (ILocation) other;
+
+        EqualsBuilder builder = new EqualsBuilder();
+        builder.append(_lineNumber, l.getLineNumber());
+        builder.append(_columnNumber, l.getColumnNumber());
+        builder.append(_resourceLocation, l.getResourceLocation());
+
+        return builder.isEquals();
+    }
+
+    public String toString()
+    {
+        if (_lineNumber <= 0 && _columnNumber <= 0)
+            return _resourceLocation.toString();
+        StringBuffer buffer = new StringBuffer(_resourceLocation.toString());
+        if (_lineNumber > 0)
+        {
+            buffer.append(", line ");
+            buffer.append(_lineNumber);
+        }
+
+        if (_columnNumber > 0)
+        {
+            buffer.append(", column ");
+            buffer.append(_columnNumber);
+        }
+
+        return buffer.toString();
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/PageRedirectException.java b/tapestry-framework/src/org/apache/tapestry/PageRedirectException.java
new file mode 100644
index 0000000..c990fef
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/PageRedirectException.java
@@ -0,0 +1,55 @@
+//  Copyright 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.tapestry;
+
+/**
+ *  Exception thrown by a {@link IComponent component} or {@link org.apache.tapestry.engine.IEngineService}
+ *  that wishes to force the application to a particular page.  This is often used
+ *  to protect a sensitive page until the user is authenticated.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ **/
+
+public class PageRedirectException extends ApplicationRuntimeException
+{
+    private String _targetPageName;
+
+    public PageRedirectException(String targetPageName)
+    {
+        this(targetPageName, null, null, targetPageName);
+    }
+
+    public PageRedirectException(IPage page)
+    {
+        this(page.getPageName());
+    }
+
+    public PageRedirectException(
+        String message,
+        Object component,
+        Throwable rootCause,
+        String targetPageName)
+    {
+        super(message, component, null, rootCause);
+
+        _targetPageName = targetPageName;
+    }
+
+    public String getTargetPageName()
+    {
+        return _targetPageName;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/RedirectException.java b/tapestry-framework/src/org/apache/tapestry/RedirectException.java
new file mode 100644
index 0000000..3bc8d44
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/RedirectException.java
@@ -0,0 +1,59 @@
+//  Copyright 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.tapestry;
+
+/**
+ *  Exception thrown to force a redirection to an arbitrary location.
+ *  This is used when, after processing a request (such as a form
+ *  submission or a link being clicked), it is desirable to go
+ *  to some arbitrary new location.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.6
+ *
+ **/
+
+public class RedirectException extends ApplicationRuntimeException
+{
+	private String _redirectLocation;
+
+	public RedirectException(String redirectLocation)
+	{
+		this(null, redirectLocation);
+	}
+
+	/** 
+	 *  @param message A message describing why the redirection is taking place.
+	 *  @param redirectLocation The location to redirect to, may be a relative path (relative
+	 *  to the {@link javax.servlet.ServletContext}).
+	 *
+	 *  @see javax.servlet.http.HttpServletResponse#sendRedirect(String)
+	 *  @see javax.servlet.http.HttpServletResponse#encodeRedirectURL(String)
+	 *
+	 **/
+
+	public RedirectException(String message, String redirectLocation)
+	{
+		super(message);
+
+		_redirectLocation = redirectLocation;
+	}
+
+	public String getRedirectLocation()
+	{
+		return _redirectLocation;
+	}
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/RedirectFilter.java b/tapestry-framework/src/org/apache/tapestry/RedirectFilter.java
new file mode 100644
index 0000000..de679b8
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/RedirectFilter.java
@@ -0,0 +1,109 @@
+//  Copyright 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.tapestry;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Filter used to redirect a root context URL (i.e., "/context" or "/context/"
+ * to the Tapestry application servlet (typically, "/context/app").  This
+ * servlet is mapped to "/" and must have a &lt;init-parameter&;gt; 
+ * <code>redirect-path</code> that is the application servlet's path (i.e.,
+ * "/app").  If no value is specified, then "/app" is used.  The path
+ * is always relative to the servlet context, and should always
+ * begin with a leading slash.
+ *  
+ * <p>Filters are only available in Servlet API 2.3 and above.
+ * 
+ * <p>Servlet API 2.4 is expected to allow a servlets in the welcome list
+ * (equivalent to index.html or index.jsp), at which point this filter
+ * should no longer be necessary.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * @since 3.0
+ */
+
+public class RedirectFilter implements Filter
+{
+    private static final Log LOG = LogFactory.getLog(RedirectFilter.class);
+    public static final String REDIRECT_PATH_PARAM = "redirect-path";
+
+    private String _redirectPath;
+
+    public void init(FilterConfig config) throws ServletException
+    {
+        _redirectPath = config.getInitParameter(REDIRECT_PATH_PARAM);
+
+        if (Tapestry.isBlank(_redirectPath))
+            _redirectPath = "/app";
+
+        if (LOG.isDebugEnabled())
+            LOG.debug(Tapestry.format("RedirectServlet.redirect-path", _redirectPath));
+    }
+
+    public void destroy()
+    {
+
+    }
+
+    /**
+     * This filter intercepts the so-called "default" servlet, whose job is
+     * to provide access to standard resources packaged within the web application
+     * context.  This code is interested in only the very root, redirecting
+     * to the appropriate Tapestry application servlet.  Other values
+     * are passed through unchanged.
+     */
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+        throws IOException, ServletException
+    {
+        HttpServletRequest hrequest = (HttpServletRequest) request;
+        HttpServletResponse hresponse = (HttpServletResponse) response;
+
+        String servletPath = hrequest.getServletPath();
+        String pathInfo = hrequest.getPathInfo();
+
+        // Been experimenting with different servlet containers.  In Jetty 4.2.8 and Tomcat 4.1,
+        // resources have a non-null servletPath.  If JBossWeb 3.0.6, the servletPath is
+        // null and the pathInfo indicates the relative location of the resource.
+
+        if ((Tapestry.isBlank(servletPath) || servletPath.equals("/"))
+            && (Tapestry.isBlank(pathInfo) || pathInfo.equals("/")))
+        {
+            String path = hrequest.getContextPath() + _redirectPath;
+
+            if (LOG.isDebugEnabled())
+                LOG.debug(Tapestry.format("RedirectServlet.redirecting", path));
+
+            hresponse.sendRedirect(path);
+            return;
+        }
+
+        chain.doFilter(request, response);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/RenderRewoundException.java b/tapestry-framework/src/org/apache/tapestry/RenderRewoundException.java
new file mode 100644
index 0000000..6c31ddc
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/RenderRewoundException.java
@@ -0,0 +1,32 @@
+//  Copyright 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.tapestry;
+
+/**
+ *  A special subclass of {@link ApplicationRuntimeException} that can be thrown
+ *  when a component has determined that the state of the page has been
+ *  rewound.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ **/
+
+public class RenderRewoundException extends ApplicationRuntimeException
+{
+    public RenderRewoundException(Object component)
+    {
+        super(null, component, null, null);
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/StaleLinkException.java b/tapestry-framework/src/org/apache/tapestry/StaleLinkException.java
new file mode 100644
index 0000000..ac10e88
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/StaleLinkException.java
@@ -0,0 +1,115 @@
+//  Copyright 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.tapestry;
+
+/**
+ *  Exception thrown by an {@link org.apache.tapestry.engine.IEngineService} when it discovers that
+ *  the an action link was for an out-of-date version of the page.
+ *
+ *  <p>The application should redirect to the StaleLink page.
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class StaleLinkException extends ApplicationRuntimeException
+{
+    private transient IPage _page;
+    private String _pageName;
+    private String _targetIdPath;
+    private String _targetActionId;
+
+    public StaleLinkException()
+    {
+        super(null, null, null, null);
+    }
+
+    /**
+     *  Constructor used when the action id is found, but the target id path
+     *  did not match the actual id path.
+     *
+     **/
+
+    public StaleLinkException(IComponent component, String targetActionId, String targetIdPath)
+    {
+        super(
+            Tapestry.format(
+                "StaleLinkException.action-mismatch",
+                new String[] { targetActionId, component.getIdPath(), targetIdPath }),
+            component,
+            null,
+            null);
+
+        _page = component.getPage();
+        _pageName = _page.getPageName();
+
+        _targetActionId = targetActionId;
+        _targetIdPath = targetIdPath;
+    }
+
+    /**
+     *  Constructor used when the target action id is not found.
+     *
+     **/
+
+    public StaleLinkException(IPage page, String targetActionId, String targetIdPath)
+    {
+        this(
+            Tapestry.format(
+                "StaleLinkException.component-mismatch",
+                targetActionId,
+                targetIdPath),
+            page);
+
+        _targetActionId = targetActionId;
+        _targetIdPath = targetIdPath;
+    }
+
+    public StaleLinkException(String message, IComponent component)
+    {
+        super(message, component, null, null);
+    }
+
+
+
+    public String getPageName()
+    {
+        return _pageName;
+    }
+
+    /**
+     *  Returns the page referenced by the service URL, if known, 
+     *  or null otherwise.
+     *
+     **/
+
+    public IPage getPage()
+    {
+        return _page;
+    }
+    
+    public String getTargetActionId()
+    {
+        return _targetActionId;
+    }
+
+    public String getTargetIdPath()
+    {
+        return _targetIdPath;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/StaleSessionException.java b/tapestry-framework/src/org/apache/tapestry/StaleSessionException.java
new file mode 100644
index 0000000..d30d372
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/StaleSessionException.java
@@ -0,0 +1,64 @@
+//  Copyright 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.tapestry;
+
+/**
+ *  Exception thrown by an {@link org.apache.tapestry.engine.IEngineService} when it discovers that
+ *  the {@link javax.servlet.http.HttpSession}
+ *  has timed out (and been replaced by a new, empty
+ *  one).
+ *
+ *  <p>The application should redirect to the stale-session page.
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+public class StaleSessionException extends ApplicationRuntimeException
+{
+    private transient IPage _page;
+    private String _pageName;
+
+    public StaleSessionException()
+    {
+        this(null, null);
+    }
+
+    public StaleSessionException(String message, IPage page)
+    {
+        super(message, page, null, null);
+        _page = page;
+
+        if (page != null)
+            _pageName = page.getPageName();
+    }
+
+    public String getPageName()
+    {
+        return _pageName;
+    }
+
+    /**
+     *  Returns the page referenced by the service URL, if known, or null otherwise.
+     *
+     **/
+
+    public IPage getPage()
+    {
+        return _page;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/Tapestry.java b/tapestry-framework/src/org/apache/tapestry/Tapestry.java
new file mode 100644
index 0000000..bcf3824
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/Tapestry.java
@@ -0,0 +1,1571 @@
+//  Copyright 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.tapestry;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.ResourceBundle;
+import java.util.Set;
+
+import javax.servlet.ServletContext;
+
+import org.apache.tapestry.event.ChangeObserver;
+import org.apache.tapestry.event.ObservedChangeEvent;
+import org.apache.tapestry.request.RequestContext;
+import org.apache.tapestry.resource.ContextResourceLocation;
+import org.apache.tapestry.spec.IComponentSpecification;
+import org.apache.tapestry.util.AdaptorRegistry;
+import org.apache.tapestry.util.StringSplitter;
+
+/**
+ *  A placeholder for a number of (static) methods that don't belong elsewhere, as well
+ *  as a global location for static constants.
+ *
+ *  @since 1.0.1
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public final class Tapestry
+{
+    /**
+     *  Name of a request attribute used with the
+     *  {@link #TAGSUPPORT_SERVICE} service.  The attribute
+     *  defines the underlying service to for which a URL will be generated.
+     *
+     *  @since 3.0
+     *
+     **/
+
+    public final static String TAG_SUPPORT_SERVICE_ATTRIBUTE =
+        "org.apache.tapestry.tagsupport.service";
+
+    /**
+     * Name of a request attribute used with the
+     * {@link #TAGSUPPORT_SERVICE} service.  The attribute
+     * defines the correct servlet path for the
+     * Tapestry application (which, for the odd-man-out TAGSUPPORT_SERVICE
+     * may not match HttpServletRequest.getServletPath() because of
+     * the use of an include.
+     *
+     * @since 3.0
+     */
+
+    public final static String TAG_SUPPORT_SERVLET_PATH_ATTRIBUTE =
+        "org.apache.tapestry.tagsupport.servlet-path";
+
+    /**
+     *  Name of a request attribute used with the
+     *  {@link #TAGSUPPORT_SERVICE} service.  The attribute
+     *  defines an array of objects to be converted into
+     *  service parameters (i.e., for use with the
+     *  {@link #EXTERNAL_SERVICE}).
+     *
+     *  @since 3.0
+     *
+     **/
+
+    public final static String TAG_SUPPORT_PARAMETERS_ATTRIBUTE =
+        "org.apache.tapestry.tagsupport.parameters";
+
+    /**
+     *  Service used to support rendering of JSP tags.  tagsupport is provided
+     *  with a service and service parameters via request attributes
+     *  and creates a URI from the result, which is output to the response.
+     *
+     *  @since 3.0
+     *
+     **/
+
+    public static final String TAGSUPPORT_SERVICE = "tagsupport";
+
+    /**
+     *  The name ("action") of a service that allows behavior to be associated with
+     *  an {@link IAction} component, such as {@link org.apache.tapestry.link.ActionLink} or
+     *  {@link org.apache.tapestry.form.Form}.
+     *
+     *  <p>This service is used with actions that are tied to the
+     *  dynamic state of the page, and which require a rewind of the page.
+     *
+     **/
+
+    public final static String ACTION_SERVICE = "action";
+
+    /**
+     *  The name ("direct") of a service that allows stateless behavior for an {@link
+     *  org.apache.tapestry.link.DirectLink} component.
+     *
+     *  <p>This service rolls back the state of the page but doesn't
+     *  rewind the the dynamic state of the page the was the action
+     *  service does, which is more efficient but less powerful.
+     *
+     *  <p>An array of String parameters may be included with the
+     *  service URL; these will be made available to the {@link org.apache.tapestry.link.DirectLink}
+     *  component's listener.
+     *
+     **/
+
+    public final static String DIRECT_SERVICE = "direct";
+
+    /**
+     *  The name ("external") of a service that a allows {@link IExternalPage} to be selected.
+     *  Associated with a {@link org.apache.tapestry.link.ExternalLink} component.
+     *
+     *  <p>This service enables {@link IExternalPage}s to be accessed via a URL.
+     *  External pages may be booked marked using their URL for future reference.
+     *
+     *  <p>An array of Object parameters may be included with the
+     *  service URL; these will be passed to the
+     *  {@link IExternalPage#activateExternalPage(Object[], IRequestCycle)} method.
+     *
+     **/
+
+    public final static String EXTERNAL_SERVICE = "external";
+
+    /**
+     *  The name ("page") of a service that allows a new page to be selected.
+     *  Associated with a {@link org.apache.tapestry.link.PageLink} component.
+     *
+     *  <p>The service requires a single parameter:  the name of the target page.
+     **/
+
+    public final static String PAGE_SERVICE = "page";
+
+    /**
+     *  The name ("home") of a service that jumps to the home page.  A stand-in for
+     *  when no service is provided, which is typically the entrypoint
+     *  to the application.
+     *
+     **/
+
+    public final static String HOME_SERVICE = "home";
+
+    /**
+     *  The name ("restart") of a service that invalidates the session and restarts
+     *  the application.  Typically used just
+     *  to recover from an exception.
+     *
+     **/
+
+    public static final String RESTART_SERVICE = "restart";
+
+    /**
+     *  The name ("asset") of a service used to access internal assets.
+     *
+     **/
+
+    public static final String ASSET_SERVICE = "asset";
+
+    /**
+     *  The name ("reset") of a service used to clear cached template
+     *  and specification data and remove all pooled pages.
+     *  This is only used when debugging as
+     *  a quick way to clear the out cached data, to allow updated
+     *  versions of specifications and templates to be loaded (without
+     *  stopping and restarting the servlet container).
+     *
+     *  <p>This service is only available if the Java system property
+     *  <code>org.apache.tapestry.enable-reset-service</code>
+     *  is set to <code>true</code>.
+     *
+     **/
+
+    public static final String RESET_SERVICE = "reset";
+
+    /**
+     *  Query parameter that identfies the service for the
+     *  request.
+     *
+     *  @since 1.0.3
+     *
+     **/
+
+    public static final String SERVICE_QUERY_PARAMETER_NAME = "service";
+
+    /**
+     *  The query parameter for application specific parameters to the
+     *  service (this is used with the direct service).  Each of these
+     *  values is encoded with {@link java.net.URLEncoder#encode(String)} before
+     *  being added to the URL.  Multiple values are handle by repeatedly
+     *  establishing key/value pairs (this is a change from behavior in
+     *  2.1 and earlier).
+     *
+     *  @since 1.0.3
+     *
+     **/
+
+    public static final String PARAMETERS_QUERY_PARAMETER_NAME = "sp";
+
+    /**
+     *  Property name used to get the extension used for templates.  This
+     *  may be set in the page or component specification, or in the page (or
+     *  component's) immediate container (library or application specification).
+     *  Unlike most properties, value isn't inherited all the way up the chain.
+     *  The default template extension is "html".
+     *
+     *  @since 3.0
+     *
+     **/
+
+    public static final String TEMPLATE_EXTENSION_PROPERTY =
+        "org.apache.tapestry.template-extension";
+
+    /**
+     *  The default extension for templates, "html".
+     *
+     *  @since 3.0
+     *
+     **/
+
+    public static final String DEFAULT_TEMPLATE_EXTENSION = "html";
+
+    /**
+     *  The name of an {@link org.apache.tapestry.IRequestCycle} attribute in which the
+     *  currently rendering {@link org.apache.tapestry.components.ILinkComponent}
+     *  is stored.  Link components do not nest.
+     *
+     **/
+
+    public static final String LINK_COMPONENT_ATTRIBUTE_NAME =
+        "org.apache.tapestry.active-link-component";
+
+    /**
+     *  Suffix appended to a parameter name to form the name of a property that stores the
+     *  binding for the parameter.
+     *
+     *  @since 3.0
+     *
+     **/
+
+    public static final String PARAMETER_PROPERTY_NAME_SUFFIX = "Binding";
+
+    /**
+     *  Name of application extension used to resolve page and component
+     *  specifications that can't be located by the normal means.  The
+     *  extension must implement
+     *  {@link org.apache.tapestry.resolver.ISpecificationResolverDelegate}.
+     *
+     *  @since 3.0
+     *
+     **/
+
+    public static final String SPECIFICATION_RESOLVER_DELEGATE_EXTENSION_NAME =
+        "org.apache.tapestry.specification-resolver-delegate";
+
+    /**
+     *  Name of application extension used to resolve page and component
+     *  templates that can't be located by the normal means.
+     *  The extension must implement
+     *  {@link org.apache.tapestry.engine.ITemplateSourceDelegate}.
+     *
+     *  @since 3.0
+     *
+     **/
+
+    public static final String TEMPLATE_SOURCE_DELEGATE_EXTENSION_NAME =
+        "org.apache.tapestry.template-source-delegate";
+
+    /**
+     *   Key used to obtain an extension from the application specification.  The extension,
+     *   if it exists, implements {@link org.apache.tapestry.request.IRequestDecoder}.
+     *
+     *   @since 2.2
+     *
+     **/
+
+    public static final String REQUEST_DECODER_EXTENSION_NAME =
+        "org.apache.tapestry.request-decoder";
+
+    /**
+     *  Name of optional application extension for the multipart decoder
+     *  used by the application.  The extension must implement
+     *  {@link org.apache.tapestry.multipart.IMultipartDecoder}
+     *  (and is generally a configured instance of
+     *  {@link org.apache.tapestry.multipart.DefaultMultipartDecoder}).
+     *
+     *  @since 3.0
+     *
+     **/
+
+    public static final String MULTIPART_DECODER_EXTENSION_NAME =
+        "org.apache.tapestry.multipart-decoder";
+
+    /**
+     * Method id used to check that {@link IPage#validate(IRequestCycle)}
+     * is invoked.
+     * @see #checkMethodInvocation(Object, String, Object)
+     * @since 3.0
+     */
+
+    public static final String ABSTRACTPAGE_VALIDATE_METHOD_ID = "AbstractPage.validate()";
+
+    /**
+     * Method id used to check that {@link IPage#detach()} is invoked.
+     * @see #checkMethodInvocation(Object, String, Object)
+     * @since 3.0
+     */
+
+    public static final String ABSTRACTPAGE_DETACH_METHOD_ID = "AbstractPage.detach()";
+
+    /**
+     *  Regular expression defining a simple property name.  Used by several different
+     *  parsers. Simple property names match Java variable names; a leading letter
+     *  (or underscore), followed by letters, numbers and underscores.
+     *
+     *  @since 3.0
+     *
+     **/
+
+    public static final String SIMPLE_PROPERTY_NAME_PATTERN = "^_?[a-zA-Z]\\w*$";
+
+    /**
+     * Name of an application extension used as a factory for
+     * {@link org.apache.tapestry.engine.IMonitor} instances.  The extension
+     * must implement {@link org.apache.tapestry.engine.IMonitorFactory}.
+     *
+     * @since 3.0
+     */
+
+    public static final String MONITOR_FACTORY_EXTENSION_NAME =
+        "org.apache.tapestry.monitor-factory";
+
+    /**
+     * Class name of an {@link ognl.TypeConverter} implementing class
+     * to use as a type converter for {@link org.apache.tapestry.binding.ExpressionBinding}
+     */
+    public static final String OGNL_TYPE_CONVERTER = "org.apache.tapestry.ognl-type-converter";
+
+    /**
+     *  Prevent instantiation.
+     *
+     **/
+
+    private Tapestry()
+    {
+    }
+
+    /**
+     *  The version of the framework; this is updated for major releases.
+     *
+     **/
+
+    public static final String VERSION = readVersion();
+
+    /**
+     *  Contains strings loaded from TapestryStrings.properties.
+     *
+     *  @since 1.0.8
+     *
+     **/
+
+    private static ResourceBundle _strings;
+
+    /**
+     *  A {@link Map} that links Locale names (as in {@link Locale#toString()} to
+     *  {@link Locale} instances.  This prevents needless duplication
+     *  of Locales.
+     *
+     **/
+
+    private static final Map _localeMap = new HashMap();
+
+    static {
+        Locale[] locales = Locale.getAvailableLocales();
+        for (int i = 0; i < locales.length; i++)
+        {
+            _localeMap.put(locales[i].toString(), locales[i]);
+        }
+    }
+
+    /**
+     *  Used for tracking if a particular super-class method has been invoked.
+     */
+
+    private static final ThreadLocal _invokedMethodIds = new ThreadLocal();
+
+    /**
+     *  A {@link org.apache.tapestry.util.AdaptorRegistry} used to coerce arbitrary objects
+     *  to boolean values.
+     *
+     *  @see #evaluateBoolean(Object)
+     **/
+
+    private static final AdaptorRegistry _booleanAdaptors = new AdaptorRegistry();
+
+    private static abstract class BoolAdaptor
+    {
+        /**
+         *  Implemented by subclasses to coerce an object to a boolean.
+         *
+         **/
+
+        public abstract boolean coerce(Object value);
+    }
+
+    private static class BooleanAdaptor extends BoolAdaptor
+    {
+        public boolean coerce(Object value)
+        {
+            Boolean b = (Boolean) value;
+
+            return b.booleanValue();
+        }
+    }
+
+    private static class NumberAdaptor extends BoolAdaptor
+    {
+        public boolean coerce(Object value)
+        {
+            Number n = (Number) value;
+
+            return n.intValue() > 0;
+        }
+    }
+
+    private static class CollectionAdaptor extends BoolAdaptor
+    {
+        public boolean coerce(Object value)
+        {
+            Collection c = (Collection) value;
+
+            return c.size() > 0;
+        }
+    }
+
+    private static class StringAdaptor extends BoolAdaptor
+    {
+        public boolean coerce(Object value)
+        {
+            String s = (String) value;
+
+            if (s.length() == 0)
+                return false;
+
+            String ts = s.trim();
+            if (ts.length() == 0)
+                return false;
+
+            // Here probably Boolean.getBoolean(s) should be used,
+            // but we need the opposite check
+            if (ts.equalsIgnoreCase("false"))
+                return false;
+
+            return true;
+        }
+    }
+
+    static {
+        _booleanAdaptors.register(Boolean.class, new BooleanAdaptor());
+        _booleanAdaptors.register(Number.class, new NumberAdaptor());
+        _booleanAdaptors.register(Collection.class, new CollectionAdaptor());
+        _booleanAdaptors.register(String.class, new StringAdaptor());
+
+        // Register a default, catch-all adaptor.
+
+        _booleanAdaptors.register(Object.class, new BoolAdaptor()
+        {
+            public boolean coerce(Object value)
+            {
+                return true;
+            }
+        });
+    }
+
+    /**
+     *  {@link AdaptorRegistry} used to extract an {@link Iterator} from
+     *  an arbitrary object.
+     *
+     **/
+
+    private static AdaptorRegistry _iteratorAdaptors = new AdaptorRegistry();
+
+    private abstract static class IteratorAdaptor
+    {
+        /**
+         *  Coeerces the object into an {@link Iterator}.
+         *
+         **/
+
+        abstract public Iterator coerce(Object value);
+    }
+
+    private static class DefaultIteratorAdaptor extends IteratorAdaptor
+    {
+        public Iterator coerce(Object value)
+        {
+            return (Iterator) value;
+        }
+
+    }
+
+    private static class CollectionIteratorAdaptor extends IteratorAdaptor
+    {
+        public Iterator coerce(Object value)
+        {
+            Collection c = (Collection) value;
+
+            if (c.size() == 0)
+                return null;
+
+            return c.iterator();
+        }
+    }
+
+    private static class ObjectIteratorAdaptor extends IteratorAdaptor
+    {
+        public Iterator coerce(Object value)
+        {
+            return Collections.singleton(value).iterator();
+        }
+    }
+
+    private static class ObjectArrayIteratorAdaptor extends IteratorAdaptor
+    {
+        public Iterator coerce(Object value)
+        {
+            Object[] array = (Object[]) value;
+
+            if (array.length == 0)
+                return null;
+
+            return Arrays.asList(array).iterator();
+        }
+    }
+
+    private static class BooleanArrayIteratorAdaptor extends IteratorAdaptor
+    {
+        public Iterator coerce(Object value)
+        {
+            boolean[] array = (boolean[]) value;
+
+            if (array.length == 0)
+                return null;
+
+            List l = new ArrayList(array.length);
+
+            for (int i = 0; i < array.length; i++)
+                l.add(array[i] ? Boolean.TRUE : Boolean.FALSE);
+
+            return l.iterator();
+        }
+    }
+
+    private static class ByteArrayIteratorAdaptor extends IteratorAdaptor
+    {
+        public Iterator coerce(Object value)
+        {
+            byte[] array = (byte[]) value;
+
+            if (array.length == 0)
+                return null;
+
+            List l = new ArrayList(array.length);
+
+            for (int i = 0; i < array.length; i++)
+                l.add(new Byte(array[i]));
+
+            return l.iterator();
+        }
+    }
+
+    private static class CharArrayIteratorAdaptor extends IteratorAdaptor
+    {
+        public Iterator coerce(Object value)
+        {
+            char[] array = (char[]) value;
+
+            if (array.length == 0)
+                return null;
+
+            List l = new ArrayList(array.length);
+
+            for (int i = 0; i < array.length; i++)
+                l.add(new Character(array[i]));
+
+            return l.iterator();
+        }
+    }
+
+    private static class ShortArrayIteratorAdaptor extends IteratorAdaptor
+    {
+        public Iterator coerce(Object value)
+        {
+            short[] array = (short[]) value;
+
+            if (array.length == 0)
+                return null;
+
+            List l = new ArrayList(array.length);
+
+            for (int i = 0; i < array.length; i++)
+                l.add(new Short(array[i]));
+
+            return l.iterator();
+        }
+    }
+
+    private static class IntArrayIteratorAdaptor extends IteratorAdaptor
+    {
+        public Iterator coerce(Object value)
+        {
+            int[] array = (int[]) value;
+
+            if (array.length == 0)
+                return null;
+
+            List l = new ArrayList(array.length);
+
+            for (int i = 0; i < array.length; i++)
+                l.add(new Integer(array[i]));
+
+            return l.iterator();
+        }
+    }
+
+    private static class LongArrayIteratorAdaptor extends IteratorAdaptor
+    {
+        public Iterator coerce(Object value)
+        {
+            long[] array = (long[]) value;
+
+            if (array.length == 0)
+                return null;
+
+            List l = new ArrayList(array.length);
+
+            for (int i = 0; i < array.length; i++)
+                l.add(new Long(array[i]));
+
+            return l.iterator();
+        }
+    }
+
+    private static class FloatArrayIteratorAdaptor extends IteratorAdaptor
+    {
+        public Iterator coerce(Object value)
+        {
+            float[] array = (float[]) value;
+
+            if (array.length == 0)
+                return null;
+
+            List l = new ArrayList(array.length);
+
+            for (int i = 0; i < array.length; i++)
+                l.add(new Float(array[i]));
+
+            return l.iterator();
+        }
+    }
+
+    private static class DoubleArrayIteratorAdaptor extends IteratorAdaptor
+    {
+        public Iterator coerce(Object value)
+        {
+            double[] array = (double[]) value;
+
+            if (array.length == 0)
+                return null;
+
+            List l = new ArrayList(array.length);
+
+            for (int i = 0; i < array.length; i++)
+                l.add(new Double(array[i]));
+
+            return l.iterator();
+        }
+    }
+
+    static {
+        _iteratorAdaptors.register(Iterator.class, new DefaultIteratorAdaptor());
+        _iteratorAdaptors.register(Collection.class, new CollectionIteratorAdaptor());
+        _iteratorAdaptors.register(Object.class, new ObjectIteratorAdaptor());
+        _iteratorAdaptors.register(Object[].class, new ObjectArrayIteratorAdaptor());
+        _iteratorAdaptors.register(boolean[].class, new BooleanArrayIteratorAdaptor());
+        _iteratorAdaptors.register(byte[].class, new ByteArrayIteratorAdaptor());
+        _iteratorAdaptors.register(char[].class, new CharArrayIteratorAdaptor());
+        _iteratorAdaptors.register(short[].class, new ShortArrayIteratorAdaptor());
+        _iteratorAdaptors.register(int[].class, new IntArrayIteratorAdaptor());
+        _iteratorAdaptors.register(long[].class, new LongArrayIteratorAdaptor());
+        _iteratorAdaptors.register(float[].class, new FloatArrayIteratorAdaptor());
+        _iteratorAdaptors.register(double[].class, new DoubleArrayIteratorAdaptor());
+    }
+
+    /**
+     *  Copys all informal {@link IBinding bindings} from a source component
+     *  to the destination component.  Informal bindings are bindings for
+     *  informal parameters.  This will overwrite parameters (formal or
+     *  informal) in the
+     *  destination component if there is a naming conflict.
+     *
+     *
+     **/
+
+    public static void copyInformalBindings(IComponent source, IComponent destination)
+    {
+        Collection names = source.getBindingNames();
+
+        if (names == null)
+            return;
+
+        IComponentSpecification specification = source.getSpecification();
+        Iterator i = names.iterator();
+
+        while (i.hasNext())
+        {
+            String name = (String) i.next();
+
+            // If not a formal parameter, then copy it over.
+
+            if (specification.getParameter(name) == null)
+            {
+                IBinding binding = source.getBinding(name);
+
+                destination.setBinding(name, binding);
+            }
+        }
+    }
+
+    /**
+     *  Evaluates an object to determine its boolean value.
+     *
+     *  <table border=1>
+     *	<tr> <th>Class</th> <th>Test</th> </tr>
+     *  <tr>
+     *		<td>{@link Boolean}</td>
+     *		<td>Self explanatory.</td>
+     *	</tr>
+     *	<tr> <td>{@link Number}</td>
+     *		<td>True if non-zero, false otherwise.</td>
+     *		</tr>
+     *	<tr>
+     *		<td>{@link Collection}</td>
+     *		<td>True if contains any elements (non-zero size), false otherwise.</td>
+     *		</tr>
+     *	<tr>
+     *		<td>{@link String}</td>
+     *		<td>True if contains any non-whitespace characters, false otherwise.</td>
+     *		</tr>
+     *	<tr>
+     *		<td>Any Object array type</td>
+     *		<td>True if contains any elements (non-zero length), false otherwise.</td>
+     *	<tr>
+     *</table>
+     *
+     * <p>Any other non-null object evaluates to true.
+     *
+     **/
+
+    public static boolean evaluateBoolean(Object value)
+    {
+        if (value == null)
+            return false;
+
+        Class valueClass = value.getClass();
+        if (valueClass.isArray())
+        {
+            Object[] array = (Object[]) value;
+
+            return array.length > 0;
+        }
+
+        BoolAdaptor adaptor = (BoolAdaptor) _booleanAdaptors.getAdaptor(valueClass);
+
+        return adaptor.coerce(value);
+    }
+
+    /**
+     *  Converts an Object into an {@link Iterator}, following some basic rules.
+     *
+     *  <table border=1>
+     * 	<tr><th>Input Class</th> <th>Result</th> </tr>
+     * <tr><td>array</td> <td>Converted to a {@link List} and iterator returned.
+     * null returned if the array is empty.  This works with both object arrays and
+     *  arrays of scalars. </td>
+     * </tr>
+     * <tr><td>{@link Iterator}</td> <td>Returned as-is.</td>
+     * <tr><td>{@link Collection}</td> <td>Iterator returned, or null
+     *  if the Collection is empty</td> </tr>
+    
+     * <tr><td>Any other</td> <td>{@link Iterator} for singleton collection returned</td> </tr>
+     * <tr><td>null</td> <td>null returned</td> </tr>
+     * </table>
+     *
+     **/
+
+    public static Iterator coerceToIterator(Object value)
+    {
+        if (value == null)
+            return null;
+
+        IteratorAdaptor adaptor = (IteratorAdaptor) _iteratorAdaptors.getAdaptor(value.getClass());
+
+        return adaptor.coerce(value);
+    }
+
+    /**
+     *  Gets the {@link Locale} for the given string, which is the result
+     *  of {@link Locale#toString()}.  If no such locale is already registered,
+     *  a new instance is created, registered and returned.
+     *
+     *
+     **/
+
+    public static Locale getLocale(String s)
+    {
+        Locale result = null;
+
+        synchronized (_localeMap)
+        {
+            result = (Locale) _localeMap.get(s);
+        }
+
+        if (result == null)
+        {
+            StringSplitter splitter = new StringSplitter('_');
+            String[] terms = splitter.splitToArray(s);
+
+            switch (terms.length)
+            {
+                case 1 :
+
+                    result = new Locale(terms[0], "");
+                    break;
+
+                case 2 :
+
+                    result = new Locale(terms[0], terms[1]);
+                    break;
+
+                case 3 :
+
+                    result = new Locale(terms[0], terms[1], terms[2]);
+                    break;
+
+                default :
+
+                    throw new IllegalArgumentException(
+                        "Unable to convert '" + s + "' to a Locale.");
+            }
+
+            synchronized (_localeMap)
+            {
+                _localeMap.put(s, result);
+            }
+
+        }
+
+        return result;
+
+    }
+
+    /**
+     *  Closes the stream (if not null), ignoring any {@link IOException} thrown.
+     *
+     *  @since 1.0.2
+     *
+     **/
+
+    public static void close(InputStream stream)
+    {
+        if (stream != null)
+        {
+            try
+            {
+                stream.close();
+            }
+            catch (IOException ex)
+            {
+                // Ignore.
+            }
+        }
+    }
+
+    /**
+     *  Gets a string from the TapestryStrings resource bundle.
+     *  The string in the bundle
+     *  is treated as a pattern for {@link MessageFormat#format(java.lang.String, java.lang.Object[])}.
+     *
+     *  @since 1.0.8
+     *
+     **/
+
+    public static String format(String key, Object[] args)
+    {
+        if (_strings == null)
+            _strings = ResourceBundle.getBundle("org.apache.tapestry.TapestryStrings");
+
+        String pattern = _strings.getString(key);
+
+        if (args == null)
+            return pattern;
+
+        return MessageFormat.format(pattern, args);
+    }
+
+    /**
+     *  Convienience method for invoking {@link #format(String, Object[])}.
+     *
+     *  @since 3.0
+     **/
+
+    public static String getMessage(String key)
+    {
+        return format(key, null);
+    }
+
+    /**
+     *  Convienience method for invoking {@link #format(String, Object[])}.
+     *
+     *  @since 3.0
+     **/
+
+    public static String format(String key, Object arg)
+    {
+        return format(key, new Object[] { arg });
+    }
+
+    /**
+     *  Convienience method for invoking {@link #format(String, Object[])}.
+     *
+     *  @since 3.0
+     *
+     **/
+
+    public static String format(String key, Object arg1, Object arg2)
+    {
+        return format(key, new Object[] { arg1, arg2 });
+    }
+
+    /**
+     *  Convienience method for invoking {@link #format(String, Object[])}.
+     *
+     *  @since 3.0
+     *
+     **/
+
+    public static String format(String key, Object arg1, Object arg2, Object arg3)
+    {
+        return format(key, new Object[] { arg1, arg2, arg3 });
+    }
+
+    private static final String UNKNOWN_VERSION = "Unknown";
+
+    /**
+     *  Invoked when the class is initialized to read the current version file.
+     *
+     **/
+
+    private static final String readVersion()
+    {
+        Properties props = new Properties();
+
+        try
+        {
+            InputStream in = Tapestry.class.getResourceAsStream("Version.properties");
+
+            if (in == null)
+                return UNKNOWN_VERSION;
+
+            props.load(in);
+
+            in.close();
+
+            return props.getProperty("framework.version", UNKNOWN_VERSION);
+        }
+        catch (IOException ex)
+        {
+            return UNKNOWN_VERSION;
+        }
+
+    }
+
+    /**
+     *  Returns the size of a collection, or zero if the collection is null.
+     *
+     *  @since 2.2
+     *
+     **/
+
+    public static int size(Collection c)
+    {
+        if (c == null)
+            return 0;
+
+        return c.size();
+    }
+
+    /**
+     *  Returns the length of the array, or 0 if the array is null.
+     *
+     *  @since 2.2
+     *
+     **/
+
+    public static int size(Object[] array)
+    {
+        if (array == null)
+            return 0;
+
+        return array.length;
+    }
+
+    /**
+     *  Returns true if the Map is null or empty.
+     *
+     *  @since 3.0
+     *
+     **/
+
+    public static boolean isEmpty(Map map)
+    {
+        return map == null || map.isEmpty();
+    }
+
+    /**
+     *  Returns true if the Collection is null or empty.
+     *
+     *  @since 3.0
+     *
+     **/
+
+    public static boolean isEmpty(Collection c)
+    {
+        return c == null || c.isEmpty();
+    }
+
+    /**
+     *  Converts a {@link Map} to an even-sized array of key/value
+     *  pairs.  This may be useful when using a Map as service parameters
+     *  (with {@link org.apache.tapestry.link.DirectLink}.  Assuming the keys
+     *  and values are simple objects (String, Boolean, Integer, etc.), then
+     *  the representation as an array will encode more efficiently
+     *  (via {@link org.apache.tapestry.util.io.DataSqueezer} than
+     *  serializing the Map and its contents.
+     *
+     *  @return the array of keys and values, or null if the input
+     *  Map is null or empty
+     *
+     *  @since 2.2
+     **/
+
+    public static Object[] convertMapToArray(Map map)
+    {
+        if (isEmpty(map))
+            return null;
+
+        Set entries = map.entrySet();
+
+        Object[] result = new Object[2 * entries.size()];
+        int x = 0;
+
+        Iterator i = entries.iterator();
+        while (i.hasNext())
+        {
+            Map.Entry entry = (Map.Entry) i.next();
+
+            result[x++] = entry.getKey();
+            result[x++] = entry.getValue();
+        }
+
+        return result;
+    }
+
+    /**
+     *  Converts an even-sized array of objects back
+     *  into a {@link Map}.
+     *
+     *  @see #convertMapToArray(Map)
+     *  @return a Map, or null if the array is null or empty
+     *  @since 2.2
+     *
+     **/
+
+    public static Map convertArrayToMap(Object[] array)
+    {
+        if (array == null || array.length == 0)
+            return null;
+
+        if (array.length % 2 != 0)
+            throw new IllegalArgumentException(getMessage("Tapestry.even-sized-array"));
+
+        Map result = new HashMap();
+
+        int x = 0;
+        while (x < array.length)
+        {
+            Object key = array[x++];
+            Object value = array[x++];
+
+            result.put(key, value);
+        }
+
+        return result;
+    }
+
+    /**
+     *  Returns the application root location, which is in the
+     *  {@link javax.servlet.ServletContext}, based on
+     *  the {@link javax.servlet.http.HttpServletRequest#getServletPath() servlet path}.
+     *
+     *  @since 3.0
+     *
+     **/
+
+    public static IResourceLocation getApplicationRootLocation(IRequestCycle cycle)
+    {
+        RequestContext context = cycle.getRequestContext();
+        ServletContext servletContext = context.getServlet().getServletContext();
+        String servletPath = context.getRequest().getServletPath();
+
+        // Could strip off the servlet name (i.e., "app" in "/app") but
+        // there's no need.
+
+        return new ContextResourceLocation(servletContext, servletPath);
+    }
+
+    /**
+     * Given a Class, creates a presentable name for the class, even if the
+     * class is a scalar type or Array type.
+     *
+     * @since 3.0
+     */
+
+    public static String getClassName(Class subject)
+    {
+        if (subject.isArray())
+            return getClassName(subject.getComponentType()) + "[]";
+
+        return subject.getName();
+    }
+
+    /**
+     *  Selects the first {@link org.apache.tapestry.ILocation} in an array of objects.
+     *  Skips over nulls.  The objects may be instances of
+     *  {Location or {@link org.apache.tapestry.ILocatable}.  May return null
+     *  if no Location found found.
+     *
+     **/
+
+    public static ILocation findLocation(Object[] locations)
+    {
+        for (int i = 0; i < locations.length; i++)
+        {
+            Object location = locations[i];
+
+            if (location == null)
+                continue;
+
+            if (location instanceof ILocation)
+                return (ILocation) location;
+
+            if (location instanceof ILocatable)
+            {
+                ILocatable locatable = (ILocatable) location;
+                ILocation result = locatable.getLocation();
+
+                if (result != null)
+                    return result;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     *  Creates an exception indicating the binding value is null.
+     *
+     *  @since 3.0
+     *
+     **/
+
+    public static BindingException createNullBindingException(IBinding binding)
+    {
+        return new BindingException(getMessage("null-value-for-binding"), binding);
+    }
+
+    /** @since 3.0 **/
+
+    public static ApplicationRuntimeException createNoSuchComponentException(
+        IComponent component,
+        String id,
+        ILocation location)
+    {
+        return new ApplicationRuntimeException(
+            format("no-such-component", component.getExtendedId(), id),
+            component,
+            location,
+            null);
+    }
+
+    /** @since 3.0 **/
+
+    public static BindingException createRequiredParameterException(
+        IComponent component,
+        String parameterName)
+    {
+        return new BindingException(
+            format("required-parameter", parameterName, component.getExtendedId()),
+            component,
+            null,
+            component.getBinding(parameterName),
+            null);
+    }
+
+    /** @since 3.0 **/
+
+    public static ApplicationRuntimeException createRenderOnlyPropertyException(
+        IComponent component,
+        String propertyName)
+    {
+        return new ApplicationRuntimeException(
+            format("render-only-property", propertyName, component.getExtendedId()),
+            component,
+            null,
+            null);
+    }
+
+    /**
+     * Clears the list of method invocations.
+     * @see #checkMethodInvocation(Object, String, Object)
+     *
+     * @since 3.0
+     */
+
+    public static void clearMethodInvocations()
+    {
+        _invokedMethodIds.set(null);
+    }
+
+    /**
+     * Adds a method invocation to the list of invocations. This is done
+     * in a super-class implementations.
+     *
+     * @see #checkMethodInvocation(Object, String, Object)
+     * @since 3.0
+     *
+     */
+
+    public static void addMethodInvocation(Object methodId)
+    {
+        List methodIds = (List) _invokedMethodIds.get();
+
+        if (methodIds == null)
+        {
+            methodIds = new ArrayList();
+            _invokedMethodIds.set(methodIds);
+        }
+
+        methodIds.add(methodId);
+    }
+
+    /**
+     * Checks to see if a particular method has been invoked.  The method is identified by a
+     * methodId (usually a String).  The methodName and object are used to create an
+     * error message.
+     *
+     * <p>
+     * The caller should invoke {@link #clearMethodInvocations()}, then invoke a method on
+     * the object.  The super-class implementation should invoke {@link #addMethodInvocation(Object)}
+     * to indicate that it was, in fact, invoked.  The caller then invokes
+     * this method to vlaidate that the super-class implementation was invoked.
+     *
+     * <p>
+     * The list of method invocations is stored in a {@link ThreadLocal} variable.
+     *
+     * @since 3.0
+     */
+
+    public static void checkMethodInvocation(Object methodId, String methodName, Object object)
+    {
+        List methodIds = (List) _invokedMethodIds.get();
+
+        if (methodIds != null && methodIds.contains(methodId))
+            return;
+
+        throw new ApplicationRuntimeException(
+            Tapestry.format(
+                "Tapestry.missing-method-invocation",
+                object.getClass().getName(),
+                methodName));
+    }
+
+    /**
+     * Method used by pages and components to send notifications about
+     * property changes.
+     *
+     * @param component the component containing the property
+     * @param propertyName the name of the property which changed
+     * @param newValue the new value for the property
+     *
+     * @since 3.0
+     */
+    public static void fireObservedChange(
+        IComponent component,
+        String propertyName,
+        Object newValue)
+    {
+        ChangeObserver observer = component.getPage().getChangeObserver();
+
+        if (observer == null)
+            return;
+
+        ObservedChangeEvent event = new ObservedChangeEvent(component, propertyName, newValue);
+
+        observer.observeChange(event);
+    }
+
+    /**
+     * Method used by pages and components to send notifications about
+     * property changes.
+     *
+     * @param component the component containing the property
+     * @param propertyName the name of the property which changed
+     * @param newValue the new value for the property
+     *
+     * @since 3.0
+     */
+    public static void fireObservedChange(
+        IComponent component,
+        String propertyName,
+        boolean newValue)
+    {
+        ChangeObserver observer = component.getPage().getChangeObserver();
+
+        if (observer == null)
+            return;
+
+        ObservedChangeEvent event =
+            new ObservedChangeEvent(
+                component,
+                propertyName,
+                newValue ? Boolean.TRUE : Boolean.FALSE);
+
+        observer.observeChange(event);
+    }
+
+    /**
+     * Method used by pages and components to send notifications about
+     * property changes.
+     *
+     * @param component the component containing the property
+     * @param propertyName the name of the property which changed
+     * @param newValue the new value for the property
+     *
+     * @since 3.0
+     */
+    public static void fireObservedChange(
+        IComponent component,
+        String propertyName,
+        double newValue)
+    {
+        ChangeObserver observer = component.getPage().getChangeObserver();
+
+        if (observer == null)
+            return;
+
+        ObservedChangeEvent event =
+            new ObservedChangeEvent(component, propertyName, new Double(newValue));
+
+        observer.observeChange(event);
+    }
+
+    /**
+     * Method used by pages and components to send notifications about
+     * property changes.
+     *
+     * @param component the component containing the property
+     * @param propertyName the name of the property which changed
+     * @param newValue the new value for the property
+     *
+     * @since 3.0
+     */
+    public static void fireObservedChange(
+        IComponent component,
+        String propertyName,
+        float newValue)
+    {
+        ChangeObserver observer = component.getPage().getChangeObserver();
+
+        if (observer == null)
+            return;
+
+        ObservedChangeEvent event =
+            new ObservedChangeEvent(component, propertyName, new Float(newValue));
+
+        observer.observeChange(event);
+    }
+
+    /**
+    * Method used by pages and components to send notifications about
+    * property changes.
+    *
+    * @param component the component containing the property
+    * @param propertyName the name of the property which changed
+    * @param newValue the new value for the property
+    *
+    * @since 3.0
+    */
+    public static void fireObservedChange(IComponent component, String propertyName, int newValue)
+    {
+        ChangeObserver observer = component.getPage().getChangeObserver();
+
+        if (observer == null)
+            return;
+
+        ObservedChangeEvent event =
+            new ObservedChangeEvent(component, propertyName, new Integer(newValue));
+
+        observer.observeChange(event);
+    }
+
+    /**
+    * Method used by pages and components to send notifications about
+    * property changes.
+    *
+    * @param component the component containing the property
+    * @param propertyName the name of the property which changed
+    * @param newValue the new value for the property
+    *
+    * @since 3.0
+    */
+    public static void fireObservedChange(IComponent component, String propertyName, long newValue)
+    {
+        ChangeObserver observer = component.getPage().getChangeObserver();
+
+        if (observer == null)
+            return;
+
+        ObservedChangeEvent event =
+            new ObservedChangeEvent(component, propertyName, new Long(newValue));
+
+        observer.observeChange(event);
+    }
+
+    /**
+     * Method used by pages and components to send notifications about
+     * property changes.
+     *
+     * @param component the component containing the property
+     * @param propertyName the name of the property which changed
+     * @param newValue the new value for the property
+     *
+     * @since 3.0
+     */
+    public static void fireObservedChange(IComponent component, String propertyName, char newValue)
+    {
+        ChangeObserver observer = component.getPage().getChangeObserver();
+
+        if (observer == null)
+            return;
+
+        ObservedChangeEvent event =
+            new ObservedChangeEvent(component, propertyName, new Character(newValue));
+
+        observer.observeChange(event);
+    }
+
+    /**
+     * Method used by pages and components to send notifications about
+     * property changes.
+     *
+     * @param component the component containing the property
+     * @param propertyName the name of the property which changed
+     * @param newValue the new value for the property
+     *
+     * @since 3.0
+     */
+    public static void fireObservedChange(IComponent component, String propertyName, byte newValue)
+    {
+        ChangeObserver observer = component.getPage().getChangeObserver();
+
+        if (observer == null)
+            return;
+
+        ObservedChangeEvent event =
+            new ObservedChangeEvent(component, propertyName, new Byte(newValue));
+
+        observer.observeChange(event);
+    }
+
+    /**
+     * Method used by pages and components to send notifications about
+     * property changes.
+     *
+     * @param component the component containing the property
+     * @param propertyName the name of the property which changed
+     * @param newValue the new value for the property
+     *
+     * @since 3.0
+     */
+    public static void fireObservedChange(
+        IComponent component,
+        String propertyName,
+        short newValue)
+    {
+        ChangeObserver observer = component.getPage().getChangeObserver();
+
+        if (observer == null)
+            return;
+
+        ObservedChangeEvent event =
+            new ObservedChangeEvent(component, propertyName, new Short(newValue));
+
+        observer.observeChange(event);
+    }
+
+    /**
+     * Returns true if the input is null or contains only whitespace.
+     * 
+     * <p>
+     * Note: Yes, you'd think we'd use <code>StringUtils</code>, but with
+     * the change in names and behavior between releases, it is smarter
+     * to just implement our own little method!
+     * 
+     * @since 3.0
+     */
+
+    public static boolean isBlank(String input)
+    {
+        if (input == null || input.length() == 0)
+            return true;
+
+        return input.trim().length() == 0;
+    }
+
+    /**
+     * Returns true if the input is not null and not empty (or only whitespace).
+     * 
+     * @since 3.0
+     * 
+     */
+
+    public static boolean isNonBlank(String input)
+    {
+        return !isBlank(input);
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/TapestryStrings.properties b/tapestry-framework/src/org/apache/tapestry/TapestryStrings.properties
new file mode 100644
index 0000000..cac3ec4
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/TapestryStrings.properties
@@ -0,0 +1,483 @@
+# $Id$
+# Copyright 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.
+#
+# Contains String contants used throughout the Tapestry framework.
+# To keep things organized, each key is in two parts:  the simple class name
+# and a subkey within the class name.
+
+
+# Some general messages
+
+service-no-parameters=Service {0} requires no service parameters.
+service-single-parameter=Service {0} requires exactly one service parameter.
+service-single-context-parameter=Service {0} requires exactly one context parameter.
+service-requires-parameters=Service {0} requires at least one service parameter.
+service-incorrect-parameter-count=Service {0} requires exactly {1} service parameters.
+missing-resource=Could not locate resource {0}.
+invalid-field-name=Invalid field name: {0}.
+unable-to-resolve-class=Unable to resolve class {0}.
+field-not-defined=Field {0} does not exist.
+illegal-field-access=Cannot access field {0}.
+field-is-instance=Field {0} is an instance variable, not a class variable.
+deprecated-component-param=Parameter ''{1}'' of component {0} is deprecated, use parameter ''{2}'' instead.
+must-be-wrapped-by-form={1} components must be enclosed by a Form component.
+invalid-null-parameter=Parameter ''{0}'' may not be null.
+null-value-for-binding=Binding value is null.
+no-such-component=Component {0} does not contain a component {1}.
+required-parameter=Value for parameter ''{0}'' in component {1} is null, and a non-null value is required.
+render-only-property=Property ''{0}'' of {1} may only be accessed while the component is rendering.
+unsupported-property=Property ''{1}'' is not supported by {0}.
+must-be-contained-by-body={0} components must be contained by a Body component.
+illegal-encoding=The encoding ''{0}'' is not recognized.
+
+# org.apache.tapestry
+
+AbstractComponent.attempt-to-change-container=Attempt to change existing container.
+AbstractComponent.attempt-to-change-component-id=Attempt to change existing component id.
+AbstractComponent.null-container={0} container is null.
+AbstractComponent.attempt-to-change-page=Attempt to change existing containing page.
+AbstractComponent.attempt-to-change-spec=Attempt to change existing component specification.
+
+AbstractPage.attempt-to-change-locale=Attempt to change existing locale for a page.
+AbstractPage.attempt-to-change-name=Attempt to change existing name for a page.
+
+AbstractMarkupWriter.missing-constructor-parameters=Incomplete parameters to AbstractMarkupWriter constructor.
+AbstractMarkupWriter.tag-not-open=A tag must be open before attributes may be set in an IMarkupWriter.
+
+ApplicationServlet.could-not-locate-engine=Could not locate an engine to service this request.
+ApplicationServlet.could-not-parse-spec=Unable to parse application specification {0}.
+ApplicationServlet.get-app-path-not-overriden=Application servlet {0} does not provide an implementation of method getApplicationServletPath().
+ApplicationServlet.no-application-specification=Running application without an application specification.
+ApplicationServlet.engine-stateful-without-session=Engine {0} is stateful even though there is no HttpSession.  Discarding the engine. 
+
+BaseComponent.multiple-component-references=Template for component {0} contains multiple references to embedded component {1}.
+BaseComponent.unbalanced-close-tags=More closing tags the open tags in template.
+BaseComponent.unbalance-open-tags=Not all tags closed in template.
+BaseComponent.missing-component-spec-single=Template for component {0} does not reference embedded component:
+BaseComponent.missing-component-spec-multi=Template for component {0} does not reference embedded components:
+BaseComponent.and=and
+BaseComponent.dupe-template-expression=An expression for parameter ''{0}'' of component {1} in the template for {2} conflicts with an existing binding in the specification.
+BaseComponent.template-expression-for-informal-parameter=The template for {2} contains an expression for parameter ''{0}'' of component {1}, but {1} does not allow informal parameters.
+BaseComponent.template-expression-for-reserved-parameter=The template for {2} contains an expression for parameter ''{0}'' of component {1}, but ''{0}'' is a reserved parameter name.
+BaseComponent.dupe-string=A localized string reference for parameter ''{0}'' of component {1} in the template for {2} conflicts with an existing binding in the specification.
+
+BaseComponentTemplateLoader.dupe-component-id=Component {0} (at {1}) conflicts with a prior declaration in the specification (at {2}).
+BaseComponentTemplateLoader.bodyless-component=This component may not have a body.
+
+RedirectServlet.redirect-path=Redirecting to servlet at path {0}.
+RedirectServlet.redirecting=Redirecting servlet context URL to {0}.
+
+ResponseOutputStream.content-type-not-set=Content type of response never set.
+
+StaleLinkException.action-mismatch=Action id {0} matched component {1}, not {2}.
+StaleLinkException.component-mismatch=Action id {0} does not match component {1}.
+
+Tapestry.even-sized-array=An even-sized array of keys and values is required.
+Tapestry.missing-method-invocation=Class {0} overrides method ''{1}'' but does not invoke the super-class implementation.
+
+# org.apache.tapestry.asset
+
+AssetExternalizer.externalize-failure=Could not externalize asset {0} to {1}.
+AssetService.exception-report-title=Failure to export asset {0}.
+AssetService.checksum-failure=Checksum {0} does not match that of resource {1}.
+AssetService.checksum-compute-failure=Failed to compute checksum for resource {1}.
+
+ExternalAsset.resource-missing=Could not access external asset {0}.
+
+# org.apache.tapestry.bean
+
+BeanProvider.bean-not-defined=Component {0} does not define a bean name {1}.
+BeanProvider.instantiation-error=Unable to instantiate bean ''{0}'' (for component {1}) as class {2}: {3}
+AbstractBeanInitializer.unable-to-set-property=Unable to set property ''{0}'' of {1} to {2}.
+
+# org.apache.tapestry.binding
+
+AbstractBinding.wrong-type=Parameter {0} ({1}) is an instance of {2}, which does not inherit from {3}.
+AbstractBinding.wrong-interface=Parameter {0} ({1}) is an instance of {2}, which does not implement interface {3}.
+AbstractBinding.read-only-binding=Binding value may not be updated.
+
+ExpressionBinding.unable-to-resolve-expression=Unable to resolve expression ''{0}'' for {1}.
+ExpressionBinding.unable-to-update-expression=Unable to update expression ''{0}'' for {1} to {2}.
+
+ListenerBinding.invalid-access=Inappropriate invocation of {0} on instance of ListenerBinding.
+ListenerBinding.bsf-exception=Unable to execute listener script "{0}": {1}
+ListenerBinding.unable-to-undeclare-bean=Unable to undeclare bean ''{0}'' after executing "{0}".
+
+# org.apache.tapestry.callback
+
+DirectCallback.wrong-type=Component {0} does not implement the IDirect interface.
+
+# org.apache.tapestry.component
+
+Insert.unable-to-format=Unable to format object {0}.
+Any.element-not-defined=The Any component is not used in a template and the 'element' property is not bound.
+
+# org.apache.tapestry.param
+
+ParameterManager.no-accessor=Component {0} does not have accessor methods for property {1}.
+ParameterManager.property-not-read-write=Property {1} of component {0} is not read-write.
+ParameterManager.java-type-not-specified=No Java type was specified for parameter {0} of component {1}.
+ParameterManager.type-mismatch=Parameter {0} of component {1} is declared as {2}, but the property is {3}.
+ParameterManager.static-initialization-failure=Unable to set property {0} of component {1} from {2}.
+ParameterManager.incompatible-direction-and-binding=Parameter {0} of component {1} is direction {2} which is incompatible with {3}.
+
+# org.apache.tapestry.engine
+
+AbstractEngine.unable-to-process-client-request=Unable to process client request.
+AbstractEngine.unable-to-present-exception-page=Unable to present exception page.
+AbstractEngine.unknown-specification=<Unknown specification>
+AbstractEngine.unknown-service=Engine does not implement a service named ''{0}''.
+AbstractEngine.unable-to-begin-request=Tapestry unable to begin processing request.
+AbstractEngine.unable-to-cleanup-page=Unable to cleanup page {0}.
+AbstractEngine.unable-to-instantiate-visit=Unable to instantiate visit object from class {0}.
+AbstractEngine.unable-to-instantiate-global=Unable to instantiate global object from class {0}.
+AbstractEngine.unable-to-redirect=Unable to redirect to {0}.
+AbstractEngine.service-name-mismatch=Class {1} is registered as service {0} but provides service {2} instead.
+AbstractEngine.unable-to-instantiate-service=Unable to instantiate class {1} as service {0}.
+AbstractEngine.unable-to-find-dispatcher=Unable to find a request dispatcher for local resource ''{0}''.
+AbstractEngine.unable-to-forward=Unable to forward to local resource ''{0}''.
+AbstractEngine.unable-to-create-cleanup-context=Unable to create an instance of RequestContext to process end-of-session page cleanups.
+AbstractEngine.exception-during-cleanup=Exception during post-request cleanup.
+AbstractEngine.exception-during-cache-clear=Exception while clearing caches after request.
+AbstractEngine.validate-cycle=A validate cycle during page activation was detected: {0}.
+
+ActionService.context-parameters=Service action requires either three or four service contect parameters.
+ActionService.action-component-wrong-type=Component {0} does not implement the IAction interface.
+
+DefaultScriptSource.unable-to-parse-script=Unable to parse script {0}.
+
+DefaultSpecificationSource.no-match-for-alias=Could not find a component matching alias {0}.
+DefaultSpecificationSource.unable-to-locate-specification=Could not locate resource {0} in the classpath.
+DefaultSpecificationSource.unable-to-open-specification=Could not open specification {0}.
+DefaultSpecificationSource.unable-to-parse-specification=Could not parse specification {0}.
+
+DefaultTemplateSource.no-template-for-component=Could not find template for component {0} in locale {1}.
+DefaultTemplateSource.no-template-for-page=Could not find template for page {0} in locale {1}.
+DefaultTemplateSource.unable-to-parse-template=Could not parse template {0}.
+DefaultTemplateSource.unable-to-read-template=Could not read template {0}.
+
+DirectService.context-parameters=Service direct requires either three or four service context parameters.
+DirectService.component-wrong-type=Component {0} does not implement the IDirect interface.
+DirectService.stale-session-exception=Component {0} is stateful, but the HttpSession has expired (or has not yet been created).
+
+EngineServiceLink.unknown-parameter-name=Unknown parameter name ''{0}''.
+
+Namespace.no-such-page=Page ''{0}'' not found in {1}.
+Namespace.no-such-component-type=Component ''{0}'' not found in {1}.
+Namespace.application-namespace=application namespace
+Namespace.framework-namespace=framework namespace
+Namespace.nested-namespace=namespace ''{0}''
+Namespace.library-id-not-found=Library ''{0}'' not found in {1}.
+
+RequestCycle.invalid-null-name=Parameter name may not be null in RequestCycle.getPage(String).
+RequestCycle.form-rewind-failure=Failure to rewind form {0}.
+
+ResourceResolver.unable-to-load-class=Could not load class {0} from {1}: {2}
+
+TagSupportService.service-only=The tagsupport service does not support tag generation.
+TagSupportService.null-attribute=Request attribute ''{0}'' is required by the tagsupport service, but the value is null.
+TagSupportService.attribute-not-string=Request attribute ''{0}'' is an instance of {1}, not a string.
+TagSupportService.attribute-not-array=Request attribute ''{0}'' is an instance of {0}, not an object array.
+BaseEngine.recorder-has-uncommited-changes=Could not forget changes to page {0} because the page's recorder has uncommitted changes.
+BaseEngine.duplicate-page-recorder=Could not create a second page recorder for page {0}.
+
+ExternalService.page-not-compatible=Page {0} does not implement the IExternalPage interface.
+
+# org.apache.tapestry.enhance
+
+ComponentClassFactory.bad-property-type=Unable to convert ''{0}'' to a property type.
+ComponentClassFactory.property-type-mismatch=Unable to enhance class {0} because it contains property ''{1}'' of type {2}, not the expected type {3}.
+ComponentClassFactory.non-abstract-read=Unable to enhance class {0} because it implements a non-abstract read method for property ''{1}''.
+ComponentClassFactory.non-abstract-write=Unable to enhance class {0} because it implements a non-abstract write method for property ''{1}''.
+ComponentClassFactory.unable-to-introspect-class=Unable to introspect properties of class {0}.
+ComponentClassFactory.auto-must-be-required=Parameter ''{0}'' must be required or have a default value as it uses direction ''auto''.
+ComponentClassFactory.code-generation-error=A code generation error occured while enhancing class {0}.
+
+
+EnhancedClassLoader.unable-to-define-class=Unable to define class {0}: {1}
+
+MethodFabricator.no-more-arguments=No more arguments may be added once any local variables are added.
+
+DefaultComponentClassEnhancer.no-impl-for-abstract-method=Method ''{0}'' (declared in {1}) has no implementation in class {2} (or enhanced subclass {3}).
+
+
+# org.apache.tapestry.event
+
+ObservedChangeEvent.null-property-name=Must specify a non-null propertyName when creating ObservedChangeEvent for {0}.
+ObservedChangeEvent.must-be-serializable=Must specify a serializable object as the new value of property when creating an ObservedChangeEvent.
+
+# org.apache.tapestry.form
+
+AbstractFormComponent.must-be-contained-by-form=This component must be contained within a Form.
+
+Form.forms-may-not-nest=Forms may not be nested.
+Form.needs-body-for-event-handlers=A Form with event handlers must be enclosed by a Body component.
+Form.too-many-ids=Rewind of form {0} expected only {1} form elements, but an additional id was requested by component {2}.
+Form.too-few-ids=Rewind of form {0} expected {1} more form elements, starting with id ''{2}''.
+Form.id-mismatch=Rewind of form {0} expected allocated id #{1} to be ''{2}'', but was ''{3}'' (requested by component {4}).
+Form.encoding-type-contention=Components within Form {0} have requested conflicting encoding types ''{1}'' and ''{2}''.
+
+ListEdit.unable-to-convert-value=Unable to convert {0} to an external string in ListEdit component.
+ListEdit.unable-to-convert-string=Unable to convert {0} back into an object in ListEdit component.
+
+FormConditional.unable-to-convert-value=Unable to convert {0} to an external string in FormConditional component.
+FormConditional.unable-to-convert-string=Unable to convert {0} back into an object in FormConditional component.
+
+Option.must-be-contained-by-select=Option component must be contained within a Select.
+
+Radio.must-be-contained-by-group=Radio component must be contained within a RadioGroup.
+
+RadioGroup.may-not-nest=RadioGroup components may not be nested.
+
+Select.may-not-nest=Select components may not be nested.
+
+LinkSubmit.may-not-nest=LinkSubmit components may not be nested.
+
+# org.apache.tapestry.html
+
+Body.may-not-nest=Body components may not be nested.
+Body.include-classpath-script-only=Unable to include external script {0}: only classpath resources are supported.
+
+InsertText.conversion-error=Error converting text to lines (for InsertText component).
+
+Rollover.must-be-contained-by-body=Rollover components must be contained within a Body component.
+Rollover.must-be-contained-by-link=Rollover components must be contained within an ILinkComponent.
+
+Script.must-be-contained-by-body=Script components must be contained within a Body component.
+
+# org.apache.tapestry.contrib.inspector
+
+ShowEngine.could-not-serialize=Could not serialize the application engine.
+
+InspectorButton.must-be-contained-by-body=InspectorButton component must be contained within a Body component.
+
+# org.apache.tapestry.jsp
+
+URLRetriever.unable-to-find-dispatcher=Unable to find request dispatcher for servlet at ''{0}''.
+URLRetriever.io-exception=I/O exception messaging servlet {0}: {1}
+URLRetriever.servlet-exception=Servlet exception messaging servlet {0}: {1}
+
+AbstractLinkTag.io-exception=I/O exception writing output: {1}
+
+AbstractTapestryTag.unable-to-evaluate-expression=Unable to evaluate OGNL expression ''{0}'': {1}
+
+# org.apache.tapestry.link
+
+GestureLink.missing-service=No engine service name {0}.
+
+AbstractLinkComponent.no-nesting=ILinkComponents may not be nested.
+AbstractLinkComponent.events-need-body=A link component with multiple functions for a single event type must be contained within a Body.
+
+# org.apache.tapestry.listener
+
+ListenerMap.object-missing-method=Object {0} does not implement a listener method named ''{1}''.
+ListenerMap.unable-to-invoke-method=Unable to invoke method {0} on {1}: {2}
+
+# org.apache.tapestry.multipart
+
+UploadPart.unable-to-open-content-file=Unable to open uploaded file ''{0}''.
+UploadPart.write-failure=Error writing uploaded content to {0}: {1}
+
+DefaultMultipartDecoder.unable-to-decode=Unable to decode request: {0}
+DefaultMultipartDecoder.encoding-not-set=No encoding has been set for this request.
+
+# org.apache.tapestry.pageload
+
+PageLoader.formal-parameters-only=Component {0} allows only formal parameters, binding {1} is not allowed.
+PageLoader.required-parameter-not-bound=Required parameter {0} of component {1} is not bound.
+PageLoader.unable-to-load-specification=Unable to load component specification.
+PageLoader.class-not-component=Class {0} does not implement the IComponent interface.
+PageLoader.unable-to-instantiate=Unable to instantiate an instance of class {0}.
+PageLoader.page-not-allowed=Component {0} may not implement the IPage interface.
+PageLoader.class-not-page=Class {0} does not implement the IPage interface.
+PageLoader.unable-to-instantiate-component=Unable to instantiate component {0}: {1}
+PageLoader.missing-asset=Unable to locate asset ''{0}'' of component {1} as {2}.
+PageLoader.unable-to-initialize-property=Unable to initialize property {0} of {1}: {2}
+PageLoader.inherit-informal-invalid-component-formal-only=Component {0} allows only formal parameters, but has inherit-informal-parameters set.
+PageLoader.inherit-informal-invalid-container-formal-only=Component {0} allows only formal parameters, but it contains component {1} that has inherit-informal-parameters set.
+
+EstablishDefaultParameterValuesVisitor.parameter-must-have-no-default-value=Parameter {1} of component {0} is required and must not have a default value.
+
+# org.apache.tapestry.parse
+
+TextToken.error-trimming={0}: Failure trimming leading and trailing whitespace.
+
+SpecificationParser.fail-convert-boolean=Could not convert ''{0}'' to boolean.
+SpecificationParser.fail-convert-int=Could not convert ''{0}'' to integer.
+SpecificationParser.fail-convert-double=Could not convert ''{0}'' to double.
+SpecificationParser.fail-convert-long=Could not convert ''{0}'' to long.
+SpecificationParser.unexpected-component-public-id=Unexpected component specification with public identifier {0}.
+SpecificationParser.unexpected-application-public-id=Unexpected application specification with public identifier {0}.
+SpecificationParser.both-type-and-copy-of=Contained component {0} contains both type and copy-of attributes.
+SpecificationParser.missing-type-or-copy-of=Contained component {0} does not specify attribute type or copy-of.
+SpecificationParser.unable-to-copy=Unable to copy component {0}, which does not exist.
+SpecificationParser.invalid-parameter-name=Parameter ''{0}'' is an invalid name.  Parameter names should be valid Java identifiers.
+SpecificationParser.invalid-page-name=''{0}'' is not a valid page name.  Page names must start with a letter and consist only of letters, numbers, period, dash and underscore.
+SpecificationParser.invalid-component-type=''{0}'' is not a valid component type.  Types must be valid Java identifiers.
+SpecificationParser.invalid-property-name=''{0}'' is not a valid JavaBean property name.  Property names must be valid Java identifiers.
+SpecificationParser.invalid-bean-name=''{0}'' is not a valid helper bean name.  Helper bean names must be valid Java identifiers.
+SpecificationParser.unknown-static-value-type=Unknown <static-value> type: ''{0}''.
+SpecificationParser.invalid-component-id=''{0}'' is not a valid component id.  Component ids must be valid Java identifiers.
+SpecificationParser.invalid-asset-name=''{0}'' is not a valid asset name.  Asset names must be valid Java identifiers.
+SpecificationParser.invalid-service-name=''{0}'' is not a valid service name.  Service names must start with a letter, and contain only letters, numbers, dash, underscore and period.
+SpecificationParser.invalid-library-id=''{0}'' is not a valid library id.  Library ids must be valid Java identifiers.
+SpecificationParser.invalid-extension-name=''{0}'' is not a valid extension name.  Extension names must start with a letter, and contain only letters, numbers, dash and underscore.
+SpecificationParser.invalid-component-type=''{0}'' is not a valid component type.
+SpecificationParser.framework-library-id-is-reserved=The library id ''{0}'' is reserved and may not be used.
+SpecificationParser.no-attribute-and-body=It is not valid to specify a value for attribute ''{0}'' of <{1}> and provide a value in the body of the element.
+SpecificationParser.required-extended-attribute=Element <{0}> does not specify a value for attribute ''{1}'', or contain a body value.
+SpecificationParser.error-reading-resource=Unable to read {0}: {1}
+
+ValidatePublicIdRule.no-public-id=Document {0} does not define a public id.
+
+TemplateParser.comment-not-ended=Comment on line {0} did not end.
+TemplateParser.unclosed-tag=Tag <{0}> on line {1} is never closed.
+TemplateParser.unclosed-unknown-tag=Tag on line {1} is never closed.
+TemplateParser.missing-attribute-value=Tag <{0}> on line {1} is missing a value for attribute {2}.
+TemplateParser.content-block-may-not-be-ignored=Tag <{0}> on line {1} is the template content, and may not be in an ignored block.
+TemplateParser.content-block-may-not-be-empty=Tag <{0}> on line {1} is the template content, and may not be empty.
+TemplateParser.unknown-component-id=Tag <{0}> on line {1} references unknown component id ''{2}''.
+TemplateParser.component-may-not-be-ignored=Tag <{0}> on line {1} is a dynamic component, and may not appear inside an ignored block.
+TemplateParser.nested-ignore=Tag <{0}> on line {1} should be ignored, but is already inside an ignored block (ignored blocks may not be nested).
+TemplateParser.incomplete-close-tag=Incomplete close tag on line {0}.
+TemplateParser.improperly-nested-close-tag=Closing tag </{0}> on line {1} is improperly nested with tag <{2}> on line {3}.
+TemplateParser.unmatched-close-tag=Closing tag </{0}> on line {1} does not have a matching open tag.
+TemplateParser.component-id-invalid=Tag <{0}> on line {1} contains an invalid jwcid ''{2}''.
+TemplateParser.duplicate-tag-attribute=Tag <{0}> on line {1} contains more than one ''{2}'' attribute.
+
+
+TextToken.range-error={0}: out of range for template length {1}.
+TemplateToken.may-not-render={0} tokens may not render.
+
+# org.apache.tapestry.record
+
+PageRecorder.change-after-lock=Page recorder for page {0} is locked after a commit(), but received a change to property {1} of component {2}.
+PageRecorder.unable-to-persist=Unable to persist property {0} of component {1} as {2}.
+PageRecorder.null-property-name=A change event for component {0} failed to specify the name of the updated property.
+PageRecorder.unable-to-rollback=Unable to set property {0} of component {1} to {2}: {3}
+
+# org.apache.tapestry.resource
+
+ContextResourceLocation.unable-to-reference-context-path=Unable to reference context path ''{0}''.
+
+
+# org.apache.tapestry.script
+
+ScriptParser.unknown-public-id=Script uses unknown public indentifier {0}.
+ScriptParser.invalid-key=''{0}'' is not a valid key.  Symbol keys must be valid Java identifiers.
+ScriptParser.unable-to-resolve-class=''{0}'' is not a resolvable class name.
+
+InputSymbolToken.required=Script symbol ''{0}'' is required, but not specified.
+InputSymbolToken.wrong-type=Script symbol ''{0}'' is {1}, not {2}.
+
+# org.apache.tapestry.spec
+
+LibrarySpecification.duplicate-child-namespace-id=A child namespace with id ''{0}'' already exists.
+LibrarySpecification.duplicate-page-name=A page named ''{0}'' already exists in this namespace.
+LibrarySpecification.duplicate-component-alias=A component alias ''{0}'' already exists in this namespace.
+LibrarySpecification.duplicate-service-name=A service named ''{0}'' already exists in this namespace.
+LibrarySpecification.duplicate-extension-name=An extension named ''{0}'' already exists in this namespace.
+LibrarySpecification.no-such-extension=No extension named ''{0}'' exists in this namespace.
+LibrarySpecification.extension-does-not-implement-interface=Extension ''{0}'' (class {1}) does not implement interface {2}.
+LibrarySpecification.extension-not-a-subclass=Extension ''{0}'' (class {1}) does not inherit from class {2}.
+
+ComponentSpecification.duplicate-asset={0}: already contains asset ''{1}''.
+ComponentSpecification.duplicate-component={0}: already contains component ''{1}''.
+ComponentSpecification.duplicate-parameter={0}: already contains parameter ''{1}''.
+ComponentSpecification.duplicate-bean={0}: already contains bean definition for ''{1}''.
+ComponentSpecification.duplicate-property-specification={0}: already contains property specification for property ''{1}''.
+
+ExtensionSpecification.duplicate-property={0}: already contains property configuration for ''{1}''.
+ExtensionSpecification.bad-class=Unable to locate class {0}.
+
+Direction.IN=in
+Direction.FORM=form
+Direction.CUSTOM=custom
+Direction.AUTO=auto
+
+# org.apache.tapestry.util
+
+AdaptorRegistry.duplicate-registration=A registration for class {0} already exists.
+AdaptorRegistry.adaptor-not-found=Could not find an adaptor for class {0}.
+
+JanitorThread.interval-locked=The interval for this janitor thread is locked.
+JanitorThread.illegal-interval=The interval for a janitor thread may not be less than 1 millisecond.
+
+MultiKey.null-keys=Must pass in non-empty array of keys.
+MultiKey.first-element-may-not-be-null=First element of keys may not be null.
+MultiKey.no-keys=No keys for this MultiKey.
+
+Pool.unable-to-instantiate-instance=Unable to instantiate new instance of class {0}.
+
+
+# org.apache.tapestry.util.io
+
+DataSqueezer.short-prefix=The adaptor prefix must contain at least one character.
+DataSqueezer.null-class=The dataClass may not be null.
+DataSqueezer.null-adaptor=The adaptor may not be null.
+DataSqueezer.prefix-out-of-range=DataSqueezer prefix must be in the range ''!'' to ''z''.
+DataSqueezer.adaptor-prefix-taken=An adaptor for prefix ''{0}'' is already registered.
+
+SerializableAdaptor.class-not-found=Class {0} not found.
+SerializableAdaptor.unable-to-convert=Cannot convert {0} into a modified Base64 character.
+SerializableAdaptor.unable-to-interpret-char=Cannot interpret ''{0}'' as a modified Base64 character.
+
+ComponentAddressAdaptor.no-separator=Invalid ComponentAddress encoding -- separator not present
+
+# org.apache.tapestry.util.prop
+
+PropertyFinder.unable-to-introspect-class=Unable to instrospect properties of class {0}.
+
+OgnlUtils.unable-to-update-expression=Unable to update expression ''{0}'' of {1} to {2}.
+OgnlUtils.unable-to-read-expression=Unable to read expression ''{0}'' of {1}.
+OgnlUtils.unable-to-parse-expression=Unable to parse expression ''{0}''.
+
+# org.apache.tapestry.util.xml
+
+AbstractDocumentParser.incorrect-document-type=Incorrect document type; expected {0} but received {1}.
+AbstractDocumentParser.unable-to-parse=Unable to parse {0}: {1}
+AbstractDocumentParser.unable-to-read=Error reading {0}: {1}
+AbstractDocumentParser.unable-to-construct-builder=Unable to construct DocumentBuilder: {0}
+AbstractDocumentParser.invalid-identifier={0} is not a valid identifier (in element {1}).
+AbstractDocumentParser.missing-resource=Resource at {0} does not exist.
+AbstractDocumentParser.unknown-public-id=Document {0} has an unexpected public id of ''{1}''.
+
+RuleDrivenParser.no-rule-for-element=No rule is defined for parsing element ''{0}''.
+RuleDrivenParser.resource-missing=Unable to find resource {0}.
+RuleDrivenParser.unable-to-open-resource=Unable to open resource {0}.
+RuleDrivenParser.parse-error=Unable to parse {0}: {1}
+
+# org.apache.tapestry.valid
+
+FieldLabel.no-display-name=Display name not specified and not provided by field {0}.
+FieldLabel.must-be-contained-by-form=This component must be contained within a Form.
+FieldLabel.no-delegate=No IValidationDelegate is available to ValidField {0}; it is specified as the delegate parameter of Form {1}.
+
+ValidField.no-delegate=No IValidationDelegate is available to ValidField {0}; it is specified as the delegate parameter of Form {1}.
+ValidField.must-be-contained-by-body=A ValidField using client-side validation must be enclosed by a Body component.
+
+NumberValidator.unknown-type=Unknown value type {0}.
+NumberValidator.no-adaptor-for-field=Unable to provide validation for field {0} (value type {1}).
+
+PatternValidator.pattern-match-error=Unable to match pattern {0} for field {1}.
+
+# org.apache.tapestry.wml
+
+Card.cards-may-not-nest=Cards may not be nested.
+Postfield.must-be-contained-by-go=This postfield must be contained within a Go.
+
+# org.apache.tapestry.contrib.components
+
+When.must-be-contained-by-choose=When component must be contained within a Choose.
diff --git a/tapestry-framework/src/org/apache/tapestry/asset/AbstractAsset.java b/tapestry-framework/src/org/apache/tapestry/asset/AbstractAsset.java
new file mode 100644
index 0000000..d69dc68
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/asset/AbstractAsset.java
@@ -0,0 +1,62 @@
+//  Copyright 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.tapestry.asset;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.tapestry.IAsset;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IResourceLocation;
+
+/**
+ *  Base class for {@link org.apache.tapestry.IAsset} implementations.  Provides
+ *  the location property.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public abstract class AbstractAsset implements IAsset
+{
+	private IResourceLocation _resourceLocation;
+    private ILocation _location;
+
+    protected AbstractAsset(IResourceLocation resourceLocation, ILocation location)
+    {
+    	_resourceLocation = resourceLocation;
+        _location = location;
+    }
+
+    public ILocation getLocation()
+    {
+        return _location;
+    }
+    
+    public IResourceLocation getResourceLocation()
+    {
+    	return _resourceLocation;
+    }
+    
+    public String toString()
+    {
+    	ToStringBuilder builder = new ToStringBuilder(this);
+    	
+    	builder.append("resourceLocation", _resourceLocation);
+    	builder.append("location", _location);
+    	
+    	return builder.toString();
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/asset/AssetExternalizer.java b/tapestry-framework/src/org/apache/tapestry/asset/AssetExternalizer.java
new file mode 100644
index 0000000..22b6600
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/asset/AssetExternalizer.java
@@ -0,0 +1,287 @@
+//  Copyright 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.tapestry.asset;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServlet;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.engine.IPropertySource;
+import org.apache.tapestry.util.StringSplitter;
+
+/**
+ *  Responsible for copying assets from the classpath to an external directory that
+ *  is visible to the web server. The externalizer is stored inside
+ *  the {@link ServletContext} as a named attribute.
+ *
+ *  <p>The externalizer uses the name <code>org.apache.tapestry.AssetExternalizer.<i>application name</i>
+ *  </code>.  It configures itself using two additional 
+ *  properties (searching in 
+ *  {@link org.apache.tapestry.IEngine#getPropertySource()}.
+ *
+ *  <table border=1>
+ *  <tr> <th>Parameter</th> <th>Description</th> </tr>
+ *  <tr valign=top> 
+ *		<td><code>org.apache.tapestry.asset.dir</code> </td>
+ * 		 <td>The directory to which assets will be copied.</td> </tr>
+ *  <tr valign=top>
+ *		<td><code>org.apache.tapestry.asset.URL</code> </td>
+ *		  <td>The corresponding URL for the asset directory.</td> </tr>
+ *	</table>
+ *
+ * <p>If either of these parameters is null, then no externalization occurs.
+ * Private assets will still be available, just less efficiently, as the application
+ * will be invoked via its servlet and, ultimately, the {@link AssetService} will need
+ * to retrieve the asset.
+ *
+ * <p>Assets maintain thier directory structure when copied.  For example,
+ * an asset with a resource path of <code>/com/skunkworx/Banner.gif</code> would
+ * be copied to the file system as <code><i>dir</i>/com/skunkworx/Banner.gif</code> and
+ * would have a URL of <code><i>URL</i>/com/skunkworx/Banner.gif</code>.
+ *
+ * <p>The externalizer will create any directories as needed.
+ *
+ * <p>The externalizer will not overwrite existing files.  When a new version of the application
+ * is deployed with changed assets, there are two deployment stategies:
+ * <ul>
+ * <li>Delete the existing asset directory and allow the externalizer to recreate and
+ * repopulate it.
+ * <li>Change the asset directory and URL, allowing the old and new assets to exist
+ *  side-by-side.
+ * </ul>
+ *
+ * <p>When using the second approach, it is best to use a directory that has
+ * a version number in it, for example, <code>D:/inetpub/assets/0</code> mapped to the URL
+ * <code>/assets/0</code>.  When a new version of the application is deployed, the trailing
+ * version number is incremented from 0 to 1.
+ * 
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class AssetExternalizer
+{
+    private static final Log LOG = LogFactory.getLog(AssetExternalizer.class);
+
+    private IResourceResolver _resolver;
+    private File _assetDir;
+    private String _URL;
+
+    /**
+     *  A map from resource path (as a String) to final URL (as a String).
+     *
+     **/
+
+    private Map _resources = new HashMap();
+
+    private static final int BUFFER_SIZE = 2048;
+
+    protected AssetExternalizer(IRequestCycle cycle)
+    {
+        _resolver = cycle.getEngine().getResourceResolver();
+    
+        IPropertySource properties = cycle.getEngine().getPropertySource();
+
+
+        String directory = properties.getPropertyValue("org.apache.tapestry.asset.dir");
+
+        if (directory == null)
+            return;
+
+        _URL = properties.getPropertyValue("org.apache.tapestry.asset.URL");
+
+        if (_URL == null)
+            return;
+
+        _assetDir = new File(directory);
+
+        LOG.debug("Initialized with directory " + _assetDir + " mapped to " + _URL);
+    }
+
+    protected void externalize(String resourcePath) throws IOException
+    {
+        String[] path;
+        int i;
+        File file;
+        StringSplitter splitter;
+        InputStream in;
+        OutputStream out;
+        int bytesRead;
+        URL inputURL;
+        byte[] buffer;
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Externalizing " + resourcePath);
+
+        file = _assetDir;
+
+        // Resources are always split by the unix seperator, even on Win32.
+
+        splitter = new StringSplitter('/');
+
+        path = splitter.splitToArray(resourcePath);
+
+        // The path is expected to start with a leading slash, but the StringSplitter
+        // will ignore that leading slash.
+
+        for (i = 0; i < path.length - 1; i++)
+        {
+            // Doing it this way makes sure the path seperators are right.
+
+            file = new File(file, path[i]);
+        }
+
+        // Make sure the directories exist.
+
+        file.mkdirs();
+
+        file = new File(file, path[path.length - 1]);
+
+        // If the file exists, then assume all is well.  This is OK for development,
+        // but there may be multithreading (or even multiprocess) race conditions
+        // around the creation of the file.
+
+        if (file.exists())
+            return;
+
+        // Get the resource and copy it to the file.
+
+        inputURL = _resolver.getResource(resourcePath);
+        if (inputURL == null)
+            throw new IOException(Tapestry.format("missing-resource", resourcePath));
+
+        in = inputURL.openStream();
+
+        out = new FileOutputStream(file);
+
+        buffer = new byte[BUFFER_SIZE];
+
+        while (true)
+        {
+            bytesRead = in.read(buffer, 0, BUFFER_SIZE);
+            if (bytesRead < 0)
+                break;
+
+            out.write(buffer, 0, bytesRead);
+        }
+
+        in.close();
+        out.close();
+
+        // The file is copied!
+    }
+
+    /**
+     *  Gets the externalizer singleton for the application.  If it does not already
+     *  exist, it is created and stored into the {@link ServletContext}.
+     *
+     *  <p>Each Tapestry application within a single {@link ServletContext}
+     *  will have its own externalizer; they are differentiated by the
+     *  application name.
+     *
+     *  @see org.apache.tapestry.spec.ApplicationSpecification#getName()
+     *
+     **/
+
+    public static AssetExternalizer get(IRequestCycle cycle)
+    {
+        HttpServlet servlet = cycle.getRequestContext().getServlet();
+        ServletContext context = servlet.getServletContext();
+
+        String servletName = servlet.getServletName();
+        
+        String attributeName = "org.apache.tapestry.AssetExternalizer:" + servletName;
+
+        AssetExternalizer result = (AssetExternalizer) context.getAttribute(attributeName);
+
+        if (result == null)
+        {
+            result = new AssetExternalizer(cycle);
+            context.setAttribute(attributeName, result);
+        }
+
+        return result;
+    }
+
+    /**
+     *  Gets the URL to a private resource.  If the resource was
+     *  previously copied out of the classpath, the previously
+     *  generated URL is returned.
+     * 
+     *  <p>If the asset directory and URL are not configured, then
+     *  returns null.
+     *
+     *  <p>Otherwise, the asset is copied out to the asset directory,
+     *  the URL is constructed (and recorded for later) and the URL is
+     *  returned.
+     *
+     *  <p>This method is not explicitly synchronized but should work
+     *  multi-threaded.  It synchronizes on the internal
+     *  <code>Map</code> used to map resource paths to URLs.
+     *
+     *  @param resourcePath The full path of the resource within the
+     *  classpath.  This is expected to include a leading slash.  For
+     *  example: <code>/com/skunkworx/Banner.gif</code>.
+     *
+     **/
+
+    public String getURL(String resourcePath)
+    {
+        String result;
+
+        if (_assetDir == null)
+            return null;
+
+        synchronized (_resources)
+        {
+            result = (String) _resources.get(resourcePath);
+
+            if (result != null)
+                return result;
+
+            try
+            {
+                externalize(resourcePath);
+            }
+            catch (IOException ex)
+            {
+                throw new ApplicationRuntimeException(
+                    Tapestry.format("AssetExternalizer.externalize-failure", resourcePath, _assetDir),
+                    ex);
+            }
+
+            result = _URL + resourcePath;
+
+            _resources.put(resourcePath, result);
+
+            return result;
+        }
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/asset/AssetService.java b/tapestry-framework/src/org/apache/tapestry/asset/AssetService.java
new file mode 100644
index 0000000..8872c6d
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/asset/AssetService.java
@@ -0,0 +1,220 @@
+//  Copyright 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.tapestry.asset;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.ServletContext;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.engine.AbstractService;
+import org.apache.tapestry.engine.IEngineServiceView;
+import org.apache.tapestry.engine.ILink;
+import org.apache.tapestry.request.ResponseOutputStream;
+
+/**
+ *  A service for building URLs to and accessing {@link org.apache.tapestry.IAsset}s.
+ *  Most of the work is deferred to the {@link org.apache.tapestry.IAsset} instance.
+ *
+ *  <p>The retrieval part is directly linked to {@link PrivateAsset}.
+ *  The service responds to a URL that encodes the path of a resource
+ *  within the classpath.  The
+ *  {@link #service(IEngineServiceView, IRequestCycle, ResponseOutputStream)}
+ *  method reads the resource and streams it out.
+ *
+ *  <p>TBD: Security issues.  Should only be able to retrieve a
+ *  resource that was previously registerred in some way
+ *  ... otherwise, hackers will be able to suck out the .class files
+ *  of the application!
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ **/
+
+public class AssetService extends AbstractService
+{
+    /**
+     *  Defaults MIME types, by extension, used when the servlet container
+     *  doesn't provide MIME types.  ServletExec Debugger, for example,
+     *  fails to do provide these.
+     *
+     **/
+
+    private final static Map _mimeTypes;
+
+    static {
+        _mimeTypes = new HashMap(17);
+        _mimeTypes.put("css", "text/css");
+        _mimeTypes.put("gif", "image/gif");
+        _mimeTypes.put("jpg", "image/jpeg");
+        _mimeTypes.put("jpeg", "image/jpeg");
+        _mimeTypes.put("htm", "text/html");
+        _mimeTypes.put("html", "text/html");
+    }
+
+    private static final int BUFFER_SIZE = 10240;
+
+    /**
+     *  Builds a {@link ILink} for a {@link PrivateAsset}.
+     *
+     *  <p>A single parameter is expected, the resource path of the asset
+     *  (which is expected to start with a leading slash).
+     *
+     **/
+
+    public ILink getLink(IRequestCycle cycle, IComponent component, Object[] parameters)
+    {
+        if (Tapestry.size(parameters) != 2)
+            throw new ApplicationRuntimeException(
+                Tapestry.format("service-incorrect-parameter-count", Tapestry.ASSET_SERVICE, new Integer(2)));
+
+        // Service is stateless
+
+        return constructLink(cycle, Tapestry.ASSET_SERVICE, null, parameters, false);
+    }
+
+    public String getName()
+    {
+        return Tapestry.ASSET_SERVICE;
+    }
+
+    private static String getMimeType(String path)
+    {
+        String key;
+        String result;
+        int dotx;
+
+        dotx = path.lastIndexOf('.');
+        key = path.substring(dotx + 1).toLowerCase();
+
+        result = (String) _mimeTypes.get(key);
+
+        if (result == null)
+            result = "text/plain";
+
+        return result;
+    }
+
+    /**
+     *  Retrieves a resource from the classpath and returns it to the
+     *  client in a binary output stream.
+     *
+     *  <p>TBD: Security issues.  Hackers can download .class files.
+     *
+     *
+     **/
+
+    public void service(
+        IEngineServiceView engine,
+        IRequestCycle cycle,
+        ResponseOutputStream output)
+        throws IOException
+    {
+        Object[] parameters = getParameters(cycle);
+
+        if (Tapestry.size(parameters) != 2)
+            throw new ApplicationRuntimeException(
+                Tapestry.format("service-incorrect-parameter-count", Tapestry.ASSET_SERVICE, new Integer(2)));
+
+        String resourcePath = (String) parameters[0];
+        String checksum = (String) parameters[1];
+
+        URL resourceURL = engine.getResourceResolver().getResource(resourcePath);
+
+        if (resourceURL == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.format("missing-resource", resourcePath));
+
+        String actualChecksum = engine.getResourceChecksumSource().getChecksum(resourceURL);
+
+        if (!actualChecksum.equals(checksum))
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("AssetService.checksum-failure", checksum, resourcePath));
+        }
+
+        URLConnection resourceConnection = resourceURL.openConnection();
+
+        ServletContext servletContext = cycle.getRequestContext().getServlet().getServletContext();
+
+        writeAssetContent(engine, cycle, output, resourcePath, resourceConnection, servletContext);
+    }
+
+    /**  @since 2.2 **/
+
+    private void writeAssetContent(
+        IEngineServiceView engine,
+        IRequestCycle cycle,
+        ResponseOutputStream output,
+        String resourcePath,
+        URLConnection resourceConnection,
+        ServletContext servletContext)
+    {
+        // Getting the content type and length is very dependant
+        // on support from the application server (represented
+        // here by the servletContext).
+
+        String contentType = servletContext.getMimeType(resourcePath);
+        int contentLength = resourceConnection.getContentLength();
+
+        try
+        {
+            if (contentLength > 0)
+                cycle.getRequestContext().getResponse().setContentLength(contentLength);
+
+            // Set the content type.  If the servlet container doesn't
+            // provide it, try and guess it by the extension.
+
+            if (contentType == null || contentType.length() == 0)
+                contentType = getMimeType(resourcePath);
+
+            output.setContentType(contentType);
+
+            // Disable any further buffering inside the ResponseOutputStream
+
+            output.forceFlush();
+
+            InputStream input = resourceConnection.getInputStream();
+
+            byte[] buffer = new byte[BUFFER_SIZE];
+
+            while (true)
+            {
+                int bytesRead = input.read(buffer);
+
+                if (bytesRead < 0)
+                    break;
+
+                output.write(buffer, 0, bytesRead);
+            }
+
+            input.close();
+        }
+        catch (Throwable ex)
+        {
+            String title = Tapestry.format("AssetService.exception-report-title", resourcePath);
+
+            engine.reportException(title, ex);
+        }
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/asset/ContextAsset.java b/tapestry-framework/src/org/apache/tapestry/asset/ContextAsset.java
new file mode 100644
index 0000000..ec62510
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/asset/ContextAsset.java
@@ -0,0 +1,80 @@
+//  Copyright 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.tapestry.asset;
+
+import java.io.InputStream;
+import java.net.URL;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IAsset;
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.resource.ContextResourceLocation;
+
+/**
+ *  An asset whose path is relative to the {@link javax.servlet.ServletContext} containing
+ *  the application.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ **/
+
+public class ContextAsset extends AbstractAsset implements IAsset
+{
+    private String _resolvedURL;
+
+    public ContextAsset(ContextResourceLocation resourceLocation, ILocation location)
+    {
+        super(resourceLocation, location);
+    }
+
+    /**
+     *  Generates a URL for the client to retrieve the asset.  The context path
+     *  is prepended to the asset path, which means that assets deployed inside
+     *  web applications will still work (if things are configured properly).
+     *
+     **/
+
+    public String buildURL(IRequestCycle cycle)
+    {
+        if (_resolvedURL == null)
+        {
+            IEngine engine = cycle.getEngine();
+            String contextPath = engine.getContextPath();
+
+            _resolvedURL = contextPath + getResourceLocation().getPath();
+        }
+        
+        return _resolvedURL;
+    }
+
+    public InputStream getResourceAsStream(IRequestCycle cycle)
+    {
+        try
+        {
+            URL url = getResourceLocation().getResourceURL();
+
+            return url.openStream();
+        }
+        catch (Exception ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("ContextAsset.resource-missing", getResourceLocation()),
+                ex);
+        }
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/asset/ExternalAsset.java b/tapestry-framework/src/org/apache/tapestry/asset/ExternalAsset.java
new file mode 100644
index 0000000..75b4c6e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/asset/ExternalAsset.java
@@ -0,0 +1,78 @@
+//  Copyright 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.tapestry.asset;
+
+import java.io.InputStream;
+import java.net.URL;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  A reference to an external URL.  {@link ExternalAsset}s are not
+ *  localizable.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class ExternalAsset extends AbstractAsset
+{
+    private String _URL;
+
+    public ExternalAsset(String URL, ILocation location)
+    {
+    	super(null, location);
+    	
+        _URL = URL;
+    }
+
+    /**
+     *  Simply returns the URL of the external asset.
+     *
+     **/
+
+    public String buildURL(IRequestCycle cycle)
+    {
+        return _URL;
+    }
+
+    public InputStream getResourceAsStream(IRequestCycle cycle)
+    {
+        URL url;
+
+        try
+        {
+            url = new URL(_URL);
+
+            return url.openStream();
+        }
+        catch (Exception ex)
+        {
+            // MalrformedURLException or IOException
+
+            throw new ApplicationRuntimeException(Tapestry.format("ExternalAsset.resource-missing", _URL), ex);
+        }
+
+    }
+
+    public String toString()
+    {
+        return "ExternalAsset[" + _URL + "]";
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/asset/PrivateAsset.java b/tapestry-framework/src/org/apache/tapestry/asset/PrivateAsset.java
new file mode 100644
index 0000000..fe2a701
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/asset/PrivateAsset.java
@@ -0,0 +1,105 @@
+//  Copyright 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.tapestry.asset;
+
+import java.io.InputStream;
+import java.net.URL;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.engine.ILink;
+import org.apache.tapestry.resource.ClasspathResourceLocation;
+
+/**
+ *  An implementation of {@link org.apache.tapestry.IAsset} for localizable assets within
+ *  the JVM's classpath.
+ *
+ *  <p>The localization code here is largely cut-and-paste from
+ *  {@link ContextAsset}.
+ *
+ *  @author Howard Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class PrivateAsset extends AbstractAsset
+{
+
+    private AssetExternalizer _externalizer;
+
+    public PrivateAsset(ClasspathResourceLocation resourceLocation, ILocation location)
+    {
+        super(resourceLocation, location);
+    }
+
+    /**
+     *  Gets the localized version of the resource.  Build
+     *  the URL for the resource.  If possible, the application's
+     *  {@link AssetExternalizer} is located, to copy the resource to
+     *  a directory visible to the web server.
+     *
+     **/
+
+    public String buildURL(IRequestCycle cycle)
+    {
+        if (_externalizer == null)
+            _externalizer = AssetExternalizer.get(cycle);
+
+        String path = getResourceLocation().getPath();
+
+        String externalURL = _externalizer.getURL(path);
+
+        if (externalURL != null)
+            return externalURL;
+
+        // Otherwise, the service is responsible for dynamically retrieving the
+        // resource.
+
+        IEngine engine = cycle.getEngine();
+        
+        URL resourceURL = engine.getResourceResolver().getResource(path);
+        String checksum = engine.getResourceChecksumSource().getChecksum(resourceURL);
+        
+        String[] parameters = new String[] { path, checksum };
+
+        AssetService service = (AssetService) engine.getService(Tapestry.ASSET_SERVICE);
+        ILink link = service.getLink(cycle, null, parameters);
+
+        return link.getURL();
+    }
+
+    public InputStream getResourceAsStream(IRequestCycle cycle)
+    {
+        IResourceLocation location = getResourceLocation();
+
+        try
+        {
+            URL url = location.getResourceURL();
+
+            return url.openStream();
+        }
+        catch (Exception ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("PrivateAsset.resource-missing", location),
+                ex);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/asset/ResourceChecksumSource.java b/tapestry-framework/src/org/apache/tapestry/asset/ResourceChecksumSource.java
new file mode 100644
index 0000000..6c21fe8
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/asset/ResourceChecksumSource.java
@@ -0,0 +1,42 @@
+// Copyright 2004, 2005 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.tapestry.asset;
+
+import java.net.URL;
+
+/**
+ * Calculates the checksum value, as a string, for a particular classpath resource. This is primarily
+ * used by the {@link org.apache.tapestry.asset.AssetService} to authenticate requests (you are not
+ * allowed access to a resource unless you can provide the correct checksum value).
+ * 
+ * This code is based on code from Howard Lewis Ship from the upcoming 3.1 release.
+ * 
+ * @author  Paul Ferraro
+ * @since   3.0.3
+ */
+public interface ResourceChecksumSource
+{
+    /**
+     * Returns the checksum value for the given resource.
+     * @param resourceURL the url of a resource
+     * @return the checksum value of the specified resource
+     */
+    public String getChecksum(URL resourceURL);
+    
+    /**
+     * Clears the internal cache.
+     */
+    public void reset();
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/asset/ResourceChecksumSourceImpl.java b/tapestry-framework/src/org/apache/tapestry/asset/ResourceChecksumSourceImpl.java
new file mode 100644
index 0000000..c9015e2
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/asset/ResourceChecksumSourceImpl.java
@@ -0,0 +1,120 @@
+//Copyright 2005 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.tapestry.asset;
+
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+import java.net.URL;
+import java.security.MessageDigest;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.codec.BinaryEncoder;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.Tapestry;
+
+/**
+ * Implementation of {@link org.apache.tapestry.asset.ResourceDigestSource} that calculates an
+ * checksum using a message digest and configured encoder.
+ * 
+ * This code is based on code from Howard Lewis Ship from the upcoming 3.1 release.
+ * 
+ * @author Paul Ferraro
+ * @since 3.0.3
+ */
+public class ResourceChecksumSourceImpl implements ResourceChecksumSource
+{
+    private static final int BUFFER_SIZE = 4096;
+
+    private Map _cache = new HashMap();
+    
+    private String _digestAlgorithm;
+
+    private BinaryEncoder _encoder;
+    
+    public ResourceChecksumSourceImpl(String digestAlgorithm, BinaryEncoder encoder)
+    {
+        _digestAlgorithm = digestAlgorithm;
+        _encoder = encoder;
+    }
+    
+    /**
+     * Checksum is obtained from cache if possible.
+     * If not, checksum is computed using {@link #computeChecksum(URL)}
+     * @see org.apache.tapestry.asset.ResourceDigestSource#getChecksum(java.net.URL)
+     */
+    public String getChecksum(URL resourceURL)
+    {
+        synchronized (_cache)
+        {
+            String checksum = (String) _cache.get(resourceURL);
+            
+            if (checksum == null)
+            {
+                checksum = computeChecksum(resourceURL);
+                
+                _cache.put(resourceURL, checksum);
+            }
+            
+            return checksum;
+        }
+    }
+    
+    /**
+     * @see org.apache.tapestry.asset.ResourceDigestSource#reset()
+     */
+    public void reset()
+    {
+        synchronized (_cache)
+        {
+            _cache.clear();
+        }
+    }
+    
+    /**
+     * Computes a message digest of the specified resource and encodes it into a string.
+     * @param resourceURL the url of a resource
+     * @return the checksum value of the specified resource
+     */
+    protected String computeChecksum(URL resourceURL)
+    {
+        try
+        {
+	        MessageDigest digest = MessageDigest.getInstance(_digestAlgorithm);
+	        
+	        InputStream inputStream = new BufferedInputStream(resourceURL.openStream(), BUFFER_SIZE);
+	        
+	        byte[] block = new byte[BUFFER_SIZE];
+	        
+	        int read = inputStream.read(block);
+	        
+	        while (read >= 0)
+	        {
+	            digest.update(block, 0, read);
+	            
+	            read = inputStream.read(block);
+	        }
+	        
+	        inputStream.close();
+	        
+	        return new String(_encoder.encode(digest.digest()));
+        }
+        catch (Exception e)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("AssetService.checksum-compute-failure", resourceURL), e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/asset/package.html b/tapestry-framework/src/org/apache/tapestry/asset/package.html
new file mode 100644
index 0000000..3b1ac80
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/asset/package.html
@@ -0,0 +1,15 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+<p>Implementations of {@link org.apache.tapestry.IAsset}, as well as
+the {@link org.apache.tapestry.asset.AssetExternalizer}, used to handle private assets.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/bean/AbstractBeanInitializer.java b/tapestry-framework/src/org/apache/tapestry/bean/AbstractBeanInitializer.java
new file mode 100644
index 0000000..cec1d08
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/bean/AbstractBeanInitializer.java
@@ -0,0 +1,67 @@
+//  Copyright 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.tapestry.bean;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.spec.BaseLocatable;
+import org.apache.tapestry.util.prop.OgnlUtils;
+
+/**
+ *  Base class for initializing a property of a JavaBean.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.5
+ * 
+ **/
+
+abstract public class AbstractBeanInitializer extends BaseLocatable implements IBeanInitializer
+{
+    protected String _propertyName;
+
+    public String getPropertyName()
+    {
+        return _propertyName;
+    }
+
+    /** @since 3.0 **/
+
+    public void setPropertyName(String propertyName)
+    {
+        _propertyName = propertyName;
+    }
+
+    protected void setBeanProperty(IResourceResolver resolver, Object bean, Object value)
+    {
+        try
+        {
+            OgnlUtils.set(_propertyName, resolver, bean, value);
+        }
+        catch (ApplicationRuntimeException ex)
+        {
+            String message =
+                Tapestry.format(
+                    "AbstractBeanInitializer.unable-to-set-property",
+                    _propertyName,
+                    bean,
+                    value);
+
+            throw new ApplicationRuntimeException(message, getLocation(), ex);
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/bean/BeanProvider.java b/tapestry-framework/src/org/apache/tapestry/bean/BeanProvider.java
new file mode 100644
index 0000000..325d081
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/bean/BeanProvider.java
@@ -0,0 +1,319 @@
+//  Copyright 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.tapestry.bean;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IBeanProvider;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.event.PageDetachListener;
+import org.apache.tapestry.event.PageEvent;
+import org.apache.tapestry.event.PageRenderListener;
+import org.apache.tapestry.spec.BeanLifecycle;
+import org.apache.tapestry.spec.IBeanSpecification;
+import org.apache.tapestry.spec.IComponentSpecification;
+
+/**
+ *  Basic implementation of the {@link IBeanProvider} interface.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.4
+ **/
+
+public class BeanProvider implements IBeanProvider, PageDetachListener, PageRenderListener
+{
+    private static final Log LOG = LogFactory.getLog(BeanProvider.class);
+
+    /**
+     *  Indicates whether this instance has been registered with its
+     *  page as a PageDetachListener.  Registration only occurs
+     *  the first time a bean with lifecycle REQUEST is instantiated.
+     *
+     **/
+
+    private boolean _registeredForDetach = false;
+
+    /**
+     *  Indicates whether this instance has been registered as a render
+     *  listener with the page.
+     * 
+     **/
+
+    private boolean _registeredForRender = false;
+
+    /**
+     *  The component for which beans are being created and tracked.
+     *
+     **/
+
+    private IComponent _component;
+
+    /**
+     *  Used for instantiating classes.
+     *
+     **/
+
+    private IResourceResolver _resolver;
+
+    /**
+     *  Map of beans, keyed on name.
+     *
+     **/
+
+    private Map _beans;
+
+    /**
+     *  Set of bean names provided by this provider.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    private Set _beanNames;
+
+    public BeanProvider(IComponent component)
+    {
+        this._component = component;
+        IEngine engine = component.getPage().getEngine();
+        _resolver = engine.getResourceResolver();
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Created BeanProvider for " + component);
+
+    }
+
+    /** @since 1.0.6 **/
+
+    public Collection getBeanNames()
+    {
+        if (_beanNames == null)
+        {
+            Collection c = _component.getSpecification().getBeanNames();
+
+            if (c == null || c.isEmpty())
+                _beanNames = Collections.EMPTY_SET;
+            else
+                _beanNames = Collections.unmodifiableSet(new HashSet(c));
+        }
+
+        return _beanNames;
+    }
+
+    /**
+     *  @since 1.0.5
+     *
+     **/
+
+    public IComponent getComponent()
+    {
+        return _component;
+    }
+
+    public Object getBean(String name)
+    {
+        Object bean = null;
+
+        if (_beans != null)
+            bean = _beans.get(name);
+
+        if (bean != null)
+            return bean;
+
+        IBeanSpecification spec = _component.getSpecification().getBeanSpecification(name);
+
+        if (spec == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "BeanProvider.bean-not-defined",
+                    _component.getExtendedId(),
+                    name));
+
+        bean = instantiateBean(name, spec);
+
+        BeanLifecycle lifecycle = spec.getLifecycle();
+
+        if (lifecycle == BeanLifecycle.NONE)
+            return bean;
+
+        if (_beans == null)
+            _beans = new HashMap();
+
+        _beans.put(name, bean);
+
+        // The first time in a request that a REQUEST lifecycle bean is created,
+        // register with the page to be notified at the end of the
+        // request cycle.
+
+        if (lifecycle == BeanLifecycle.REQUEST && !_registeredForDetach)
+        {
+            _component.getPage().addPageDetachListener(this);
+            _registeredForDetach = true;
+        }
+
+        if (lifecycle == BeanLifecycle.RENDER && !_registeredForRender)
+        {
+            _component.getPage().addPageRenderListener(this);
+            _registeredForRender = true;
+        }
+
+        // No need to register if a PAGE lifecycle bean; those can stick around
+        // forever.
+
+        return bean;
+    }
+
+    private Object instantiateBean(String beanName, IBeanSpecification spec)
+    {
+        String className = spec.getClassName();
+        Object bean = null;
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Instantiating instance of " + className);
+
+        // Do it the hard way!
+
+        try
+        {
+            Class beanClass = _resolver.findClass(className);
+
+            bean = beanClass.newInstance();
+        }
+        catch (Exception ex)
+        {
+
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "BeanProvider.instantiation-error",
+                    new Object[] {
+                        beanName,
+                        _component.getExtendedId(),
+                        className,
+                        ex.getMessage()}),
+                spec.getLocation(),
+                ex);
+        }
+
+        // OK, have the bean, have to initialize it.
+
+        List initializers = spec.getInitializers();
+
+        if (initializers == null)
+            return bean;
+
+        Iterator i = initializers.iterator();
+        while (i.hasNext())
+        {
+            IBeanInitializer iz = (IBeanInitializer) i.next();
+
+            if (LOG.isDebugEnabled())
+                LOG.debug("Initializing property " + iz.getPropertyName());
+
+            iz.setBeanProperty(this, bean);
+        }
+
+        return bean;
+    }
+
+    /**
+     *  Removes all beans with the REQUEST lifecycle.  Beans with
+     *  the PAGE lifecycle stick around, and beans with no lifecycle
+     *  were never stored in the first place.
+     *
+     **/
+
+    public void pageDetached(PageEvent event)
+    {
+        removeBeans(BeanLifecycle.REQUEST);
+    }
+
+    /**
+     *  Removes any beans with the specified lifecycle.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    private void removeBeans(BeanLifecycle lifecycle)
+    {
+        if (_beans == null)
+            return;
+
+        IComponentSpecification spec = null;
+
+        Iterator i = _beans.entrySet().iterator();
+        while (i.hasNext())
+        {
+            Map.Entry e = (Map.Entry) i.next();
+            String name = (String) e.getKey();
+
+            if (spec == null)
+                spec = _component.getSpecification();
+
+            IBeanSpecification s = spec.getBeanSpecification(name);
+
+            if (s.getLifecycle() == lifecycle)
+            {
+                Object bean = e.getValue();
+
+                if (LOG.isDebugEnabled())
+                    LOG.debug("Removing " + lifecycle.getName() + " bean " + name + ": " + bean);
+
+                i.remove();
+            }
+        }
+    }
+
+    /** @since 1.0.8 **/
+
+    public IResourceResolver getResourceResolver()
+    {
+        return _resolver;
+    }
+
+    /** @since 2.2 **/
+
+    public void pageBeginRender(PageEvent event)
+    {
+    }
+
+    /** @since 2.2 **/
+
+    public void pageEndRender(PageEvent event)
+    {
+        removeBeans(BeanLifecycle.RENDER);
+    }
+
+    /** @since 2.2 **/
+
+    public boolean canProvideBean(String name)
+    {
+        return getBeanNames().contains(name);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/bean/BeanProviderPropertyAccessor.java b/tapestry-framework/src/org/apache/tapestry/bean/BeanProviderPropertyAccessor.java
new file mode 100644
index 0000000..02fbf26
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/bean/BeanProviderPropertyAccessor.java
@@ -0,0 +1,75 @@
+//  Copyright 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.tapestry.bean;
+
+import java.util.Map;
+
+import ognl.ObjectPropertyAccessor;
+import ognl.OgnlException;
+
+import org.apache.tapestry.IBeanProvider;
+
+/**
+ *  Adapts a {@link org.apache.tapestry.IBeanProvider} to
+ *  <a href="http://www.ognl.org">OGNL</a> by exposing the named
+ *  beans provided by the provider as read-only properties of
+ *  the provider.
+ * 
+ *  <p>This is registered by {@link org.apache.tapestry.AbstractComponent}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.2
+ *
+ **/
+
+public class BeanProviderPropertyAccessor extends ObjectPropertyAccessor
+{
+    /**
+     *  Checks to see if the name matches the name of a bean inside
+     *  the provider and returns that bean if so.
+     *  Otherwise, invokes the super implementation.
+     * 
+     **/
+    
+    public Object getProperty(Map context, Object target, Object name) throws OgnlException
+    {
+        IBeanProvider provider = (IBeanProvider)target;
+        String beanName = (String)name;
+        
+        if (provider.canProvideBean(beanName))
+            return provider.getBean(beanName);
+        
+        return super.getProperty(context, target, name);
+    }
+
+    /**
+     *  Returns true if the name matches a bean provided by the provider.
+     *  Otherwise invokes the super implementation.
+     * 
+     **/
+    
+    public boolean hasGetProperty(Map context, Object target, Object oname) throws OgnlException
+    {
+        IBeanProvider provider = (IBeanProvider)target;
+        String beanName = (String)oname;
+
+        if (provider.canProvideBean(beanName))
+            return true;
+            
+        return super.hasGetProperty(context, target, oname);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/bean/Default.java b/tapestry-framework/src/org/apache/tapestry/bean/Default.java
new file mode 100644
index 0000000..b1ecd74
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/bean/Default.java
@@ -0,0 +1,89 @@
+//  Copyright 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.tapestry.bean;
+
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.util.pool.IPoolable;
+
+/**
+ *  A helper bean to assist with providing defaults for unspecified
+ *  parameters.    It is initalized
+ *  with an {@link IBinding} and a default value.  It's value property
+ *  is either the value of the binding, but if the binding is null,
+ *  or the binding returns null, the default value is returned.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.5
+ * 
+ **/
+
+public class Default implements IPoolable
+{
+    private IBinding binding;
+    private Object defaultValue;
+
+    public void resetForPool()
+    {
+        binding = null;
+        defaultValue = null;
+    }
+
+    public void setBinding(IBinding value)
+    {
+        binding = value;
+    }
+
+    public IBinding getBinding()
+    {
+        return binding;
+    }
+
+    public void setDefaultValue(Object value)
+    {
+        defaultValue = value;
+    }
+
+    public Object getDefaultValue()
+    {
+        return defaultValue;
+    }
+
+    /**
+     *  Returns the value of the binding.  However, if the binding is null, or the binding
+     *  returns null, then the defaultValue is returned instead.
+     *
+     **/
+
+    public Object getValue()
+    {
+        if (binding == null)
+            return defaultValue;
+
+        Object value = binding.getObject();
+
+        if (value == null)
+            return defaultValue;
+
+        return value;
+    }
+    
+    /** @since 3.0 **/
+    
+    public void discardFromPool()
+    {
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/bean/EvenOdd.java b/tapestry-framework/src/org/apache/tapestry/bean/EvenOdd.java
new file mode 100644
index 0000000..22639c8
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/bean/EvenOdd.java
@@ -0,0 +1,61 @@
+//  Copyright 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.tapestry.bean;
+
+/**
+ *  Used to emit a stream of alterating string values: "even", "odd", etc.  This
+ *  is often used in the Inspector pages to make the class of a &lt;tr&gt; alternate
+ *  for presentation reasons.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public class EvenOdd
+{
+    private boolean even = true;
+
+    /**
+     *  Returns "even" or "odd".  Whatever it returns on one invocation, it will
+     *  return the opposite on the next.  By default, the first value
+     *  returned is "even".
+     *
+     **/
+
+    public String getNext()
+    {
+        String result = even ? "even" : "odd";
+
+        even = !even;
+
+        return result;
+    }
+    
+    public boolean isEven()
+    {
+        return even;
+    }
+
+	/**
+	 *  Overrides the even flag.
+	 * 
+	 **/
+	
+    public void setEven(boolean value)
+    {
+        even = value;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/bean/ExpressionBeanInitializer.java b/tapestry-framework/src/org/apache/tapestry/bean/ExpressionBeanInitializer.java
new file mode 100644
index 0000000..390465d
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/bean/ExpressionBeanInitializer.java
@@ -0,0 +1,61 @@
+//  Copyright 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.tapestry.bean;
+
+import org.apache.tapestry.IBeanProvider;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.util.prop.OgnlUtils;
+
+/**
+ * 
+ *  Initializes a helper bean property from an OGNL expression (relative
+ *  to the bean's {@link IComponent}).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.2
+ *
+ **/
+
+public class ExpressionBeanInitializer extends AbstractBeanInitializer
+{
+    protected String _expression;
+
+    public void setBeanProperty(IBeanProvider provider, Object bean)
+    {
+        IResourceResolver resolver = provider.getResourceResolver();
+        IComponent component = provider.getComponent();
+        
+        Object value = OgnlUtils.get(_expression, resolver, component);
+
+        setBeanProperty(resolver, bean, value);
+    }
+
+	/** @since 3.0 **/
+	
+    public String getExpression()
+    {
+        return _expression;
+    }
+
+	/** @since 3.0 **/
+	
+    public void setExpression(String expression)
+    {
+        _expression = expression;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/bean/FieldBeanInitializer.java b/tapestry-framework/src/org/apache/tapestry/bean/FieldBeanInitializer.java
new file mode 100644
index 0000000..7666ce8
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/bean/FieldBeanInitializer.java
@@ -0,0 +1,119 @@
+//  Copyright 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.tapestry.bean;
+
+import java.lang.reflect.Field;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IBeanProvider;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Initializes a bean with the value of a public static field.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.8
+ *
+ **/
+
+public class FieldBeanInitializer extends AbstractBeanInitializer
+{
+    protected String _fieldName;
+    protected Object _fieldValue;
+    private boolean _fieldResolved = false;
+
+    public synchronized void setBeanProperty(IBeanProvider provider, Object bean)
+    {
+        IResourceResolver resolver = provider.getResourceResolver();
+
+        if (!_fieldResolved)
+            resolveField(resolver);
+
+        setBeanProperty(resolver, bean, _fieldValue);
+    }
+
+    private void resolveField(IResourceResolver resolver)
+    {
+        if (_fieldResolved)
+            return;
+
+        // This is all copied out of of FieldBinding!!
+
+        int dotx = _fieldName.lastIndexOf('.');
+
+        if (dotx < 0)
+            throw new ApplicationRuntimeException(
+                Tapestry.format("invalid-field-name", _fieldName));
+
+        String className = _fieldName.substring(0, dotx);
+        String simpleFieldName = _fieldName.substring(dotx + 1);
+
+        // Simple class names are assumed to be in the java.lang package.
+
+        if (className.indexOf('.') < 0)
+            className = "java.lang." + className;
+
+        Class targetClass = null;
+
+        try
+        {
+            targetClass = resolver.findClass(className);
+        }
+        catch (Throwable t)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("unable-to-resolve-class", className),
+                t);
+        }
+
+        Field field = null;
+
+        try
+        {
+            field = targetClass.getField(simpleFieldName);
+        }
+        catch (NoSuchFieldException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("field-not-defined", _fieldName),
+                ex);
+        }
+
+        // Get the value of the field.  null means look for it as a static
+        // variable.
+
+        try
+        {
+            _fieldValue = field.get(null);
+        }
+        catch (IllegalAccessException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("illegal-field-access", _fieldName),
+                ex);
+        }
+        catch (NullPointerException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("field-is-instance", _fieldName),
+                ex);
+        }
+
+        _fieldResolved = true;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/bean/IBeanInitializer.java b/tapestry-framework/src/org/apache/tapestry/bean/IBeanInitializer.java
new file mode 100644
index 0000000..6ccf1da
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/bean/IBeanInitializer.java
@@ -0,0 +1,46 @@
+//  Copyright 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.tapestry.bean;
+
+import org.apache.tapestry.IBeanProvider;
+import org.apache.tapestry.ILocationHolder;
+
+/**
+ *  Interface for a set of classes used to initialize helper beans.
+ *
+ *  @author Howard Ship
+ *  @version $Id$
+ *  @since 1.0.5
+ * 
+ **/
+
+public interface IBeanInitializer extends ILocationHolder
+{
+    /**
+     *  Invoked by the {@link IBeanProvider} to initialize
+     *  a property of the bean.
+     *
+     **/
+
+    public void setBeanProperty(IBeanProvider provider, Object bean);
+
+    /**
+     *  Returns the name of the property this initializer
+     *  will set.
+     *
+     **/
+
+    public String getPropertyName();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/bean/MessageBeanInitializer.java b/tapestry-framework/src/org/apache/tapestry/bean/MessageBeanInitializer.java
new file mode 100644
index 0000000..bae2270
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/bean/MessageBeanInitializer.java
@@ -0,0 +1,57 @@
+//  Copyright 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.tapestry.bean;
+
+import org.apache.tapestry.IBeanProvider;
+import org.apache.tapestry.IComponent;
+
+/**
+ *  A bean initializer that uses a localized string from the containing
+ *  component.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.2
+ * 
+ **/
+
+public class MessageBeanInitializer extends AbstractBeanInitializer
+{
+    protected String _key;
+	
+
+    public void setBeanProperty(IBeanProvider provider, Object bean)
+    {
+        IComponent component = provider.getComponent();
+        String value = component.getMessage(_key);
+        
+        setBeanProperty(provider.getResourceResolver(), bean, value);
+    }
+    
+    /** @since 3.0 **/
+    
+    public String getKey()
+    {
+        return _key;
+    }
+
+	/** @since 3.0 **/
+	
+    public void setKey(String key)
+    {
+        _key = key;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/bean/StaticBeanInitializer.java b/tapestry-framework/src/org/apache/tapestry/bean/StaticBeanInitializer.java
new file mode 100644
index 0000000..9183f56
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/bean/StaticBeanInitializer.java
@@ -0,0 +1,36 @@
+//  Copyright 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.tapestry.bean;
+
+import org.apache.tapestry.IBeanProvider;
+
+/**
+ *  Initializes a bean with a static value.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.5
+ * 
+ **/
+
+public class StaticBeanInitializer extends AbstractBeanInitializer
+{
+    protected Object _value;
+
+    public void setBeanProperty(IBeanProvider provider, Object bean)
+    {
+        setBeanProperty(provider.getResourceResolver(), bean, _value);
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/bean/package.html b/tapestry-framework/src/org/apache/tapestry/bean/package.html
new file mode 100644
index 0000000..2c03eb9
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/bean/package.html
@@ -0,0 +1,16 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+<p>Contains useful helper beans, an implementation of
+the {@link org.apache.tapestry.IBeanProvider} interface, and 
+several interfaces and classes related to initializing helper beans.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/binding/AbstractBinding.java b/tapestry-framework/src/org/apache/tapestry/binding/AbstractBinding.java
new file mode 100644
index 0000000..4b13b2c
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/binding/AbstractBinding.java
@@ -0,0 +1,245 @@
+//  Copyright 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.tapestry.binding;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.tapestry.BindingException;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Base class for {@link IBinding} implementations.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * 
+ **/
+
+public abstract class AbstractBinding implements IBinding
+{
+    /** @since 3.0 **/
+
+    private ILocation _location;
+
+    /**
+     *  A mapping from primitive types to wrapper types.
+     * 
+     **/
+
+    private static final Map PRIMITIVE_TYPES = new HashMap();
+
+    static {
+        PRIMITIVE_TYPES.put(boolean.class, Boolean.class);
+        PRIMITIVE_TYPES.put(byte.class, Byte.class);
+        PRIMITIVE_TYPES.put(char.class, Character.class);
+        PRIMITIVE_TYPES.put(short.class, Short.class);
+        PRIMITIVE_TYPES.put(int.class, Integer.class);
+        PRIMITIVE_TYPES.put(long.class, Long.class);
+        PRIMITIVE_TYPES.put(float.class, Float.class);
+        PRIMITIVE_TYPES.put(double.class, Double.class);
+    }
+
+    /** @since 3.0 **/
+
+    protected AbstractBinding(ILocation location)
+    {
+        _location = location;
+    }
+
+    public ILocation getLocation()
+    {
+        return _location;
+    }
+
+    /**
+     *  Cooerces the raw value into a true or false, according to the
+     *  rules set by {@link Tapestry#evaluateBoolean(Object)}.
+     *
+     **/
+
+    public boolean getBoolean()
+    {
+        return Tapestry.evaluateBoolean(getObject());
+    }
+
+    public int getInt()
+    {
+        Object raw;
+
+        raw = getObject();
+        if (raw == null)
+            throw Tapestry.createNullBindingException(this);
+
+        if (raw instanceof Number)
+        {
+            return ((Number) raw).intValue();
+        }
+
+        if (raw instanceof Boolean)
+        {
+            return ((Boolean) raw).booleanValue() ? 1 : 0;
+        }
+
+        // Save parsing for last.  This may also throw a number format exception.
+
+        return Integer.parseInt((String) raw);
+    }
+
+    public double getDouble()
+    {
+        Object raw;
+
+        raw = getObject();
+        if (raw == null)
+            throw Tapestry.createNullBindingException(this);
+
+        if (raw instanceof Number)
+        {
+            return ((Number) raw).doubleValue();
+        }
+
+        if (raw instanceof Boolean)
+        {
+            return ((Boolean) raw).booleanValue() ? 1 : 0;
+        }
+
+        // Save parsing for last.  This may also throw a number format exception.
+
+        return Double.parseDouble((String) raw);
+    }
+
+    /**
+     *  Gets the value for the binding.  If null, returns null,
+     *  otherwise, returns the String (<code>toString()</code>) version of
+     *  the value.
+     *
+     **/
+
+    public String getString()
+    {
+        Object value;
+
+        value = getObject();
+        if (value == null)
+            return null;
+
+        return value.toString();
+    }
+
+    /**
+     *  @throws BindingException always.
+     *
+     **/
+
+    public void setBoolean(boolean value)
+    {
+        throw createReadOnlyBindingException(this);
+    }
+
+    /**
+     *  @throws BindingException always.
+     *
+     **/
+
+    public void setInt(int value)
+    {
+        throw createReadOnlyBindingException(this);
+    }
+
+    /**
+     *  @throws BindingException always.
+     *
+     **/
+
+    public void setDouble(double value)
+    {
+        throw createReadOnlyBindingException(this);
+    }
+
+    /**
+     *  @throws BindingException always.
+     *
+     **/
+
+    public void setString(String value)
+    {
+        throw createReadOnlyBindingException(this);
+    }
+
+    /**
+     *  @throws BindingException always.
+     *
+     **/
+
+    public void setObject(Object value)
+    {
+        throw createReadOnlyBindingException(this);
+    }
+
+    /**
+     *  Default implementation: returns true.
+     * 
+     *  @since 2.0.3
+     * 
+     **/
+
+    public boolean isInvariant()
+    {
+        return true;
+    }
+
+    public Object getObject(String parameterName, Class type)
+    {
+        Object result = getObject();
+
+        if (result == null)
+            return result;
+
+        Class resultClass = result.getClass();
+
+        if (type.isAssignableFrom(resultClass))
+            return result;
+
+        if (type.isPrimitive() && isWrapper(type, resultClass))
+            return result;
+
+        String key =
+            type.isInterface() ? "AbstractBinding.wrong-interface" : "AbstractBinding.wrong-type";
+
+        String message =
+            Tapestry.format(
+                key,
+                new Object[] { parameterName, result, resultClass.getName(), type.getName()});
+
+        throw new BindingException(message, this);
+    }
+
+    public boolean isWrapper(Class primitiveType, Class subjectClass)
+    {
+        return PRIMITIVE_TYPES.get(primitiveType).equals(subjectClass);
+    }
+
+    /** @since 3.0 **/
+
+    protected BindingException createReadOnlyBindingException(IBinding binding)
+    {
+        return new BindingException(
+            Tapestry.getMessage("AbstractBinding.read-only-binding"),
+            binding);
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/binding/ExpressionBinding.java b/tapestry-framework/src/org/apache/tapestry/binding/ExpressionBinding.java
new file mode 100644
index 0000000..15419e5
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/binding/ExpressionBinding.java
@@ -0,0 +1,600 @@
+//  Copyright 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.tapestry.binding;
+
+import java.util.Map;
+
+import ognl.Ognl;
+import ognl.OgnlException;
+import ognl.TypeConverter;
+
+import org.apache.tapestry.BindingException;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.spec.BeanLifecycle;
+import org.apache.tapestry.spec.IBeanSpecification;
+import org.apache.tapestry.spec.IApplicationSpecification;
+import org.apache.tapestry.util.StringSplitter;
+import org.apache.tapestry.util.prop.OgnlUtils;
+
+/**
+ *  Implements a dynamic binding, based on getting and fetching
+ *  values using JavaBeans property access.  This is built
+ *  upon the <a href="http://www.ognl.org">OGNL</a> library.
+ *
+ *  <p><b>Optimization of the Expression</b>
+ * 
+ *  <p>There's a lot of room for optimization here because we can
+ *  count on some portions of the expression to be
+ *  effectively static.  Note that we type the root object as
+ *  {@link IComponent}.  We have some expectations that
+ *  certain properties of the root (and properties reachable from the root)
+ *  will be constant for the lifetime of the binding.  For example, 
+ *  components never change thier page or container.  This means
+ *  that certain property prefixes can be optimized:
+ *
+ *  <ul>
+ *  <li>page
+ *  <li>container
+ *  <li>components.<i>name</i>
+ *  </ul>
+ *
+ *  <p>This means that once an ExpressionBinding has been triggered, 
+ *  the {@link #toString()} method may return different values for the root
+ *  component and the expression than was originally set.
+ * 
+ *  <p><b>Identifying Invariants</b>
+ * 
+ *  <p>Most expressions are fully dynamic; they must be
+ *  resolved each time they are accessed.  This can be somewhat inefficient.
+ *  Tapestry can identify certain paths as invariant:
+ * 
+ *  <ul>
+ *  <li>A component within the page hierarchy 
+ *  <li>An {@link org.apache.tapestry.IAsset} from then assets map (property <code>assets</code>)
+ *  <li>A {@link org.apache.tapestry.IActionListener}
+ *  from the listener map (property <code>listeners</code>)
+ *  <li>A bean with a {@link org.apache.tapestry.spec.BeanLifecycle#PAGE}
+ *  lifecycle (property <code>beans</code>)
+ *  <li>A binding (property <code>bindings</code>)
+ *  </ul>
+ * 
+ *  <p>
+ *  These optimizations have some inherent dangers; they assume that
+ *  the components have not overidden the specified properties;
+ *  the last one (concerning helper beans) assumes that the
+ *  component does inherit from {@link org.apache.tapestry.AbstractComponent}.
+ *  If this becomes a problem in the future, it may be necessary to
+ *  have the component itself involved in these determinations.
+ *  
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.2
+ * 
+ **/
+
+public class ExpressionBinding extends AbstractBinding
+{
+    /**
+     *  The root object against which the nested property name is evaluated.
+     *
+     **/
+
+    private IComponent _root;
+
+    /**
+     *  The OGNL expression, as a string.
+     *
+     **/
+
+    private String _expression;
+
+    /**
+     *  If true, then the binding is invariant, and cachedValue
+     *  is the ultimate value.
+     * 
+     **/
+
+    private boolean _invariant = false;
+
+    /**
+     *  Stores the cached value for the binding, if invariant
+     *  is true.
+     * 
+     **/
+
+    private Object _cachedValue;
+
+    /**
+     *   Parsed OGNL expression.
+     * 
+     **/
+
+    private Object _parsedExpression;
+
+    /**
+     *  Flag set once the binding has initialized.
+     *  _cachedValue, _invariant and _final value
+     *  for _expression
+     *  are not valid until after initialization.
+     * 
+     * 
+     **/
+
+    private boolean _initialized;
+
+    private IResourceResolver _resolver;
+
+    /**
+     *  The OGNL context for this binding.  It is retained
+     *  for the lifespan of the binding once created.
+     * 
+     **/
+
+    private Map _context;
+
+    /**
+     *  Creates a {@link ExpressionBinding} from the root object
+     *  and an OGNL expression.
+     * 
+     **/
+
+    public ExpressionBinding(
+        IResourceResolver resolver,
+        IComponent root,
+        String expression,
+        ILocation location)
+    {
+        super(location);
+
+        _resolver = resolver;
+        _root = root;
+        _expression = expression;
+    }
+
+    public String getExpression()
+    {
+        return _expression;
+    }
+
+    public IComponent getRoot()
+    {
+        return _root;
+    }
+
+    /**
+     *  Gets the value of the property path, with the assistance of a 
+     *  OGNL.
+     *
+     *  @throws BindingException if an exception is thrown accessing the property.
+     *
+     **/
+
+    public Object getObject()
+    {
+        initialize();
+
+        if (_invariant)
+            return _cachedValue;
+
+        return resolveProperty();
+    }
+
+    private Object resolveProperty()
+    {
+        try
+        {
+            return Ognl.getValue(_parsedExpression, getOgnlContext(), _root);
+        }
+        catch (OgnlException t)
+        {
+            throw new BindingException(
+                Tapestry.format(
+                    "ExpressionBinding.unable-to-resolve-expression",
+                    _expression,
+                    _root),
+                this,
+                t);
+        }
+    }
+
+    /**
+     *  Creates an OGNL context used to get or set a value.
+     *  We may extend this in the future to set additional
+     *  context variables (such as page, request cycle and engine).
+     *  An optional type converter will be added to the OGNL context
+     *  if it is specified as an application extension with the name
+     *  {@link Tapestry#OGNL_TYPE_CONVERTER}.
+     * 
+     **/
+
+    private Map getOgnlContext()
+    {
+        if (_context == null)
+            _context = Ognl.createDefaultContext(_root, _resolver);
+
+        if (_root.getPage() != null)
+        {
+            if (_root.getPage().getEngine() != null)
+            {
+                IApplicationSpecification appSpec = _root.getPage().getEngine().getSpecification();
+
+                if (appSpec != null && appSpec.checkExtension(Tapestry.OGNL_TYPE_CONVERTER))
+                {
+                    TypeConverter typeConverter =
+                        (TypeConverter) appSpec.getExtension(
+                            Tapestry.OGNL_TYPE_CONVERTER,
+                            TypeConverter.class);
+
+                    Ognl.setTypeConverter(_context, typeConverter);
+                }
+            }
+        }
+
+        return _context;
+    }
+
+    /**
+     *  Returns true if the binding is expected to always 
+     *  return the same value.
+     * 
+     * 
+     **/
+
+    public boolean isInvariant()
+    {
+        initialize();
+
+        return _invariant;
+    }
+
+    public void setBoolean(boolean value)
+    {
+        setObject(value ? Boolean.TRUE : Boolean.FALSE);
+    }
+
+    public void setInt(int value)
+    {
+        setObject(new Integer(value));
+    }
+
+    public void setDouble(double value)
+    {
+        setObject(new Double(value));
+    }
+
+    public void setString(String value)
+    {
+        setObject(value);
+    }
+
+    /**
+     *  Sets up the helper object, but also optimizes the property path
+     *  and determines if the binding is invarant.
+     *
+     **/
+
+    private void initialize()
+    {
+        if (_initialized)
+            return;
+
+        _initialized = true;
+
+        try
+        {
+            _parsedExpression = OgnlUtils.getParsedExpression(_expression);
+        }
+        catch (Exception ex)
+        {
+            throw new BindingException(ex.getMessage(), this, ex);
+        }
+
+        if (checkForConstant())
+            return;
+
+        try
+        {
+            if (!Ognl.isSimpleNavigationChain(_parsedExpression, getOgnlContext()))
+                return;
+        }
+        catch (OgnlException ex)
+        {
+            throw new BindingException(ex.getMessage(), this, ex);
+        }
+
+        // Split the expression into individual property names.
+        // We then optimize what we can from the expression.  This will
+        // shorten the expression and, in some cases, eliminate
+        // it.  We also check to see if the binding can be an invariant.
+
+        String[] split = new StringSplitter('.').splitToArray(_expression);
+
+        int count = optimizeRootObject(split);
+
+        // We'ver removed some or all of the initial elements of split
+        // but have to account for anthing left over.
+
+        if (count == split.length)
+        {
+            // The property path was something like "page" or "component.foo"
+            // and was completely eliminated.
+
+            _expression = null;
+            _parsedExpression = null;
+
+            _invariant = true;
+            _cachedValue = _root;
+
+            return;
+        }
+
+        _expression = reassemble(count, split);
+        _parsedExpression = OgnlUtils.getParsedExpression(_expression);
+
+        checkForInvariant(count, split);
+    }
+
+    /**
+     *  Looks for common prefixes on the expression (provided pre-split) that
+     *  are recognized as references to other components.
+     * 
+     *  @return the number of leading elements of the split expression that
+     *  have been removed.
+     * 
+     **/
+
+    private int optimizeRootObject(String[] split)
+    {
+        int i;
+
+        for (i = 0; i < split.length; i++)
+        {
+
+            if (split[i].equals("page"))
+            {
+                _root = _root.getPage();
+                continue;
+            }
+
+            if (split[i].equals("container"))
+            {
+                _root = _root.getContainer();
+                continue;
+            }
+
+            // Here's the tricky one ... if its of the form
+            // "components.foo" we can get the named component
+            // directly.
+
+            if (split[i].equals("components") && i + 1 < split.length)
+            {
+                _root = _root.getComponent(split[i + 1]);
+                i++;
+                continue;
+            }
+
+            // Not a recognized prefix, break the loop
+
+            break;
+        }
+
+        return i;
+    }
+
+    private boolean checkForConstant()
+    {
+        try
+        {
+            if (Ognl.isConstant(_parsedExpression, getOgnlContext()))
+            {
+                _invariant = true;
+
+                _cachedValue = resolveProperty();
+
+                return true;
+            }
+        }
+        catch (OgnlException ex)
+        {
+            throw new BindingException(
+                Tapestry.format(
+                    "ExpressionBinding.unable-to-resolve-expression",
+                    _expression,
+                    _root),
+                this,
+                ex);
+        }
+
+        return false;
+    }
+
+    /**
+     *  Reassembles the remainder of the split property path
+     *  from the start point.
+     * 
+     **/
+
+    private String reassemble(int start, String[] split)
+    {
+        int count = split.length - start;
+
+        if (count == 0)
+            return null;
+
+        if (count == 1)
+            return split[split.length - 1];
+
+        StringBuffer buffer = new StringBuffer();
+
+        for (int i = start; i < split.length; i++)
+        {
+            if (i > start)
+                buffer.append('.');
+
+            buffer.append(split[i]);
+        }
+
+        return buffer.toString();
+    }
+
+    /**
+     *  Checks to see if the binding can be converted to an invariant.
+     * 
+     **/
+
+    private void checkForInvariant(int start, String[] split)
+    {
+        // For now, all of our conditions are two properties
+        // from a root component.
+
+        if (split.length - start != 2)
+            return;
+
+        try
+        {
+            if (!Ognl.isSimpleNavigationChain(_parsedExpression, getOgnlContext()))
+                return;
+        }
+        catch (OgnlException ex)
+        {
+            throw new BindingException(
+                Tapestry.format(
+                    "ExpressionBinding.unable-to-resolve-expression",
+                    _expression,
+                    _root),
+                this,
+                ex);
+        }
+
+        String first = split[start];
+
+        if (first.equals("listeners"))
+        {
+            _invariant = true;
+
+            // Could cast to AbstractComponent, get listenersMap, etc.,
+            // but this is easier.
+
+            _cachedValue = resolveProperty();
+            return;
+        }
+
+        if (first.equals("assets"))
+        {
+            String name = split[start + 1];
+
+            _invariant = true;
+            _cachedValue = _root.getAsset(name);
+            return;
+        }
+
+        if (first.equals("beans"))
+        {
+            String name = split[start + 1];
+
+            IBeanSpecification bs = _root.getSpecification().getBeanSpecification(name);
+
+            if (bs == null || bs.getLifecycle() != BeanLifecycle.PAGE)
+                return;
+
+            // Again, could cast to AbstractComponent, but this
+            // is easier.
+
+            _invariant = true;
+            _cachedValue = resolveProperty();
+            return;
+        }
+
+        if (first.equals("bindings"))
+        {
+            String name = split[start + 1];
+
+            _invariant = true;
+            _cachedValue = _root.getBinding(name);
+            return;
+        }
+
+        // Not a recognized pattern for conversion
+        // to invariant.
+    }
+
+    /**
+     *  Updates the property for the binding to the given value.  
+     *
+     *  @throws BindingException if the property can't be updated (typically
+     *  due to an security problem, or a missing mutator method).
+     *  @throws BindingException if the binding is invariant.
+     **/
+
+    public void setObject(Object value)
+    {
+        initialize();
+
+        if (_invariant)
+            throw createReadOnlyBindingException(this);
+
+        try
+        {
+            Ognl.setValue(_parsedExpression, getOgnlContext(), _root, value);
+        }
+        catch (OgnlException ex)
+        {
+            throw new BindingException(
+                Tapestry.format(
+                    "ExpressionBinding.unable-to-update-expression",
+                    _expression,
+                    _root,
+                    value),
+                this,
+                ex);
+        }
+    }
+
+    /**
+     *  Returns the a String representing the property path.  This includes
+     *  the {@link IComponent#getExtendedId() extended id} of the root component
+     *  and the property path ... once the binding is used, these may change
+     *  due to optimization of the property path.
+     *
+     **/
+
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer();
+
+        buffer.append("ExpressionBinding[");
+        buffer.append(_root.getExtendedId());
+
+        if (_expression != null)
+        {
+            buffer.append(' ');
+            buffer.append(_expression);
+        }
+
+        if (_invariant)
+        {
+            buffer.append(" cachedValue=");
+            buffer.append(_cachedValue);
+        }
+
+        buffer.append(']');
+
+        return buffer.toString();
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/binding/FieldBinding.java b/tapestry-framework/src/org/apache/tapestry/binding/FieldBinding.java
new file mode 100644
index 0000000..249f243
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/binding/FieldBinding.java
@@ -0,0 +1,149 @@
+//  Copyright 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.tapestry.binding;
+
+import java.lang.reflect.Field;
+
+import org.apache.tapestry.BindingException;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *
+ *  A type of static {@link org.apache.tapestry.IBinding} that gets it value from a public field
+ *  (static class variable) of some class or interface.
+ *
+ *  <p>The binding uses a field name, which consists of a fully qualified class name and
+ *  a static field of that class seperated by a dot.  For example: <code>com.foobar.SomeClass.SOME_FIELD</code>.
+ *
+ *  <p>If the class specified is for the <code>java.lang</code> package, then the package may be
+ *  ommitted.  This allows <code>Boolean.TRUE</code> to be recognized as a valid value.
+ *
+ *  <p>The {@link org.apache.tapestry.engine.IPageSource} maintains a cache of FieldBindings.  This means that
+ *  each field will be represented by a single binding ... that means that for any field,
+ *  the <code>accessValue()</code> method (which obtains the value for the field using
+ *  reflection) will only be invoked once.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * @deprecated To be removed in 2.5 with no replacement.  Can be accomplished using OGNL expressions.
+ * 
+ **/
+
+public class FieldBinding extends AbstractBinding
+{
+    private String fieldName;
+    private boolean accessed;
+    private Object value;
+    private IResourceResolver resolver;
+
+    public FieldBinding(IResourceResolver resolver, String fieldName, ILocation location)
+    {
+    	super(location);
+    	
+        this.resolver = resolver;
+        this.fieldName = fieldName;
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer;
+
+        buffer = new StringBuffer("FieldBinding[");
+        buffer.append(fieldName);
+
+        if (accessed)
+        {
+            buffer.append(" (");
+            buffer.append(value);
+            buffer.append(')');
+        }
+
+        buffer.append(']');
+
+        return buffer.toString();
+    }
+
+    public Object getObject()
+    {
+        if (!accessed)
+            accessValue();
+
+        return value;
+    }
+
+    private void accessValue()
+    {
+        String className;
+        String simpleFieldName;
+        int dotx;
+        Class targetClass;
+        Field field;
+
+        dotx = fieldName.lastIndexOf('.');
+
+        if (dotx < 0)
+            throw new BindingException(Tapestry.format("invalid-field-name", fieldName), this);
+
+        // Hm. Should validate that there's a dot!
+
+        className = fieldName.substring(0, dotx);
+        simpleFieldName = fieldName.substring(dotx + 1);
+
+        // Simple class names are assumed to be in the java.lang package.
+
+        if (className.indexOf('.') < 0)
+            className = "java.lang." + className;
+
+        try
+        {
+            targetClass = resolver.findClass(className);
+        }
+        catch (Throwable t)
+        {
+            throw new BindingException(Tapestry.format("unable-to-resolve-class", className), this, t);
+        }
+
+        try
+        {
+            field = targetClass.getField(simpleFieldName);
+        }
+        catch (NoSuchFieldException ex)
+        {
+            throw new BindingException(Tapestry.format("field-not-defined", fieldName), this, ex);
+        }
+
+        // Get the value of the field.  null means look for it as a static
+        // variable.
+
+        try
+        {
+            value = field.get(null);
+        }
+        catch (IllegalAccessException ex)
+        {
+            throw new BindingException(Tapestry.format("illegal-field-acccess", fieldName), this, ex);
+        }
+        catch (NullPointerException ex)
+        {
+            throw new BindingException(Tapestry.format("field-is-instance", fieldName), this, ex);
+        }
+
+        // Don't look for it again, even if the value is itself null.
+
+        accessed = true;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/binding/ListenerBinding.java b/tapestry-framework/src/org/apache/tapestry/binding/ListenerBinding.java
new file mode 100644
index 0000000..fd24bd8
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/binding/ListenerBinding.java
@@ -0,0 +1,206 @@
+//  Copyright 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.tapestry.binding;
+
+import org.apache.bsf.BSFException;
+import org.apache.bsf.BSFManager;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.BindingException;
+import org.apache.tapestry.IActionListener;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IPage;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.util.pool.Pool;
+
+/**
+ *  A very specialized binding that can be used as an {@link org.apache.tapestry.IActionListener},
+ *  executing a script in a scripting language, via
+ *  <a href="http://jakarta.apache.org/bsf">Bean Scripting Framework</a>.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class ListenerBinding extends AbstractBinding implements IActionListener
+{
+    private static final Log LOG = LogFactory.getLog(ListenerBinding.class);
+
+    private static final String BSF_POOL_KEY = "org.apache.tapestry.BSFManager";
+
+    private String _language;
+    private String _script;
+    private IComponent _component;
+
+    public ListenerBinding(IComponent component, String language, String script, ILocation location)
+    {
+        super(location);
+
+        _component = component;
+        _language = language;
+        _script = script;
+    }
+
+    /**
+     *  Always returns true.
+     * 
+     **/
+
+    public boolean getBoolean()
+    {
+        return true;
+    }
+
+    public int getInt()
+    {
+        throw new BindingException(
+            Tapestry.format("ListenerBinding.invalid-access", "getInt()"),
+            this);
+    }
+
+    public double getDouble()
+    {
+        throw new BindingException(
+            Tapestry.format("ListenerBinding.invalid-access", "getDouble()"),
+            this);
+
+    }
+
+    /**
+     *  Returns the underlying script.
+     * 
+     **/
+
+    public String getString()
+    {
+        return _script;
+    }
+
+    /**
+     *  Returns this.
+     * 
+     **/
+
+    public Object getObject()
+    {
+        return this;
+    }
+
+    /**
+     *  A ListenerBinding is also a {@link org.apache.tapestry.IActionListener}.  It
+     *  registers a number of beans with the BSF manager and invokes the
+     *  script.
+     * 
+     *  <p>
+     *  Registers the following bean:
+     *  <ul>
+     *  <li>component - the relevant {@link IComponent}, typically the same as the page 
+     *  <li>page - the {@link IPage} trigged by the request (obtained by {@link IRequestCycle#getPage()}
+     *  <li>cycle - the {@link IRequestCycle}, from which can be found
+     *  the {@link IEngine}, etc.
+     *  </ul>
+     * 
+     **/
+
+    public void actionTriggered(IComponent component, IRequestCycle cycle)
+    {
+        boolean debug = LOG.isDebugEnabled();
+
+        long startTime = debug ? System.currentTimeMillis() : 0;
+
+        BSFManager bsf = obtainBSFManager(cycle);
+
+        ILocation location = getLocation();
+
+        try
+        {
+            IPage page = cycle.getPage();
+
+            bsf.declareBean("component", _component, _component.getClass());
+            bsf.declareBean("page", page, page.getClass());
+            bsf.declareBean("cycle", cycle, cycle.getClass());
+
+            bsf.exec(
+                _language,
+                location.getResourceLocation().toString(),
+                location.getLineNumber(),
+                location.getLineNumber(),
+                _script);
+        }
+        catch (BSFException ex)
+        {
+            String message =
+                Tapestry.format("ListenerBinding.bsf-exception", location, ex.getMessage());
+
+            throw new ApplicationRuntimeException(message, _component, getLocation(), ex);
+        }
+        finally
+        {
+            if (LOG.isDebugEnabled())
+                LOG.debug("Cleaning up " + bsf);
+
+            undeclare(bsf, "component");
+            undeclare(bsf, "page");
+            undeclare(bsf, "cycle");
+
+            cycle.getEngine().getPool().store(BSF_POOL_KEY, bsf);
+
+            if (debug)
+            {
+                long endTime = System.currentTimeMillis();
+
+                LOG.debug(
+                    "Execution of \"" + location + "\" took " + (endTime - startTime) + " millis");
+            }
+        }
+    }
+
+    private void undeclare(BSFManager bsf, String name)
+    {
+        try
+        {
+            bsf.undeclareBean(name);
+        }
+        catch (BSFException ex)
+        {
+            LOG.warn(Tapestry.format("ListenerBinding.unable-to-undeclare-bean", ex));
+        }
+    }
+
+    private BSFManager obtainBSFManager(IRequestCycle cycle)
+    {
+        IEngine engine = cycle.getEngine();
+        Pool pool = engine.getPool();
+
+        BSFManager result = (BSFManager) pool.retrieve(BSF_POOL_KEY);
+
+        if (result == null)
+        {
+            LOG.debug("Creating new BSFManager instance.");
+
+            result = new BSFManager();
+
+            result.setClassLoader(engine.getResourceResolver().getClassLoader());
+        }
+
+        return result;
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/binding/StaticBinding.java b/tapestry-framework/src/org/apache/tapestry/binding/StaticBinding.java
new file mode 100644
index 0000000..80bf953
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/binding/StaticBinding.java
@@ -0,0 +1,90 @@
+//  Copyright 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.tapestry.binding;
+
+import org.apache.tapestry.ILocation;
+
+/**
+ * Stores a static (invariant) String as the value.
+ *
+ * <p>It may be useful to cache static bindings the way {@link FieldBinding}s are cached.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * 
+ **/
+
+public class StaticBinding extends AbstractBinding
+{
+    private String _value;
+    private boolean _parsedInt;
+    private int _intValue;
+    private boolean _parsedDouble;
+    private double _doubleValue;
+
+    public StaticBinding(String value, ILocation location)
+    {
+    	super(location);
+    	
+        _value = value;
+    }
+
+    /**
+     *  Interprets the static value as an integer.
+     *
+     **/
+
+    public int getInt()
+    {
+        if (!_parsedInt)
+        {
+            _intValue = Integer.parseInt(_value);
+            _parsedInt = true;
+        }
+
+        return _intValue;
+    }
+
+    /**
+     *  Interprets the static value as a double.
+     *
+     **/
+
+    public double getDouble()
+    {
+        if (!_parsedDouble)
+        {
+            _doubleValue = Double.parseDouble(_value);
+            _parsedDouble = true;
+        }
+
+        return _doubleValue;
+    }
+
+    public String getString()
+    {
+        return _value;
+    }
+
+    public Object getObject()
+    {
+        return _value;
+    }
+
+    public String toString()
+    {
+        return "StaticBinding[" + _value + "]";
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/binding/StringBinding.java b/tapestry-framework/src/org/apache/tapestry/binding/StringBinding.java
new file mode 100644
index 0000000..fd9ff7e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/binding/StringBinding.java
@@ -0,0 +1,87 @@
+//  Copyright 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.tapestry.binding;
+
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.ILocation;
+
+/**
+ *  A binding that connects directly to a localized string for
+ *  a component.
+ *
+ *  @see IComponent#getString(String)
+ * 
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.0.4
+ *
+ **/
+
+public class StringBinding extends AbstractBinding
+{
+    private IComponent _component;
+    private String _key;
+
+    public StringBinding(IComponent component, String key, ILocation location)
+    {
+    	super(location);
+    	
+        _component = component;
+        _key = key;
+    }
+
+    public IComponent getComponent()
+    {
+        return _component;
+    }
+
+    public String getKey()
+    {
+        return _key;
+    }
+
+    /**
+     *  Accesses the specified localized string.  Never returns null.
+     *
+     **/
+
+    public Object getObject()
+    {
+        return _component.getMessages().getMessage(_key);
+    }
+
+    /**
+     *  Returns true.  Localized component strings are
+     *  read-only.
+     * 
+     **/
+
+    public boolean isInvariant()
+    {
+        return true;
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer("StringBinding");
+        buffer.append('[');
+        buffer.append(_component.getExtendedId());
+        buffer.append(' ');
+        buffer.append(_key);
+        buffer.append(']');
+
+        return buffer.toString();
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/binding/package.html b/tapestry-framework/src/org/apache/tapestry/binding/package.html
new file mode 100644
index 0000000..a893063
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/binding/package.html
@@ -0,0 +1,14 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+<p>Implementations of {@link org.apache.tapestry.IBinding}.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/callback/DirectCallback.java b/tapestry-framework/src/org/apache/tapestry/callback/DirectCallback.java
new file mode 100644
index 0000000..8b890df
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/callback/DirectCallback.java
@@ -0,0 +1,117 @@
+//  Copyright 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.tapestry.callback;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IDirect;
+import org.apache.tapestry.IPage;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Simple callback for re-invoking a {@link IDirect} component trigger..
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *  @since 0.2.9
+ *
+ **/
+
+public class DirectCallback implements ICallback
+{
+    /**
+     *  @since 2.0.4
+     * 
+     **/
+
+    private static final long serialVersionUID = -8888847655917503471L;
+
+    private String _pageName;
+    private String _componentIdPath;
+    private Object[] _parameters;
+
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer("DirectCallback[");
+
+        buffer.append(_pageName);
+        buffer.append('/');
+        buffer.append(_componentIdPath);
+
+        if (_parameters != null)
+        {
+            String sep = " ";
+
+            for (int i = 0; i < _parameters.length; i++)
+            {
+                buffer.append(sep);
+                buffer.append(_parameters[i]);
+
+                sep = ", ";
+            }
+        }
+
+        buffer.append(']');
+
+        return buffer.toString();
+
+    }
+
+    /**
+     *  Creates a new DirectCallback for the component.  The parameters
+     *  (which may be null) is retained, not copied.
+     *
+     **/
+
+    public DirectCallback(IDirect component, Object[] parameters)
+    {
+        _pageName = component.getPage().getPageName();
+        _componentIdPath = component.getIdPath();
+        _parameters = parameters;
+    }
+
+    /**
+     *  Locates the {@link IDirect} component that was previously identified
+     *  (and whose page and id path were stored).
+     *  Invokes {@link IRequestCycle#setServiceParameters(Object[])} to
+     *  restore the service parameters, then
+     *  invokes {@link IDirect#trigger(IRequestCycle)} on the component.
+     *
+     **/
+
+    public void performCallback(IRequestCycle cycle)
+    {
+        IPage page = cycle.getPage(_pageName);
+        IComponent component = page.getNestedComponent(_componentIdPath);
+        IDirect direct = null;
+
+        try
+        {
+            direct = (IDirect) component;
+        }
+        catch (ClassCastException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("DirectCallback.wrong-type", component.getExtendedId()),
+                component,
+                null,
+                ex);
+        }
+
+        cycle.setServiceParameters(_parameters);
+        direct.trigger(cycle);
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/callback/ExternalCallback.java b/tapestry-framework/src/org/apache/tapestry/callback/ExternalCallback.java
new file mode 100644
index 0000000..80d50c3
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/callback/ExternalCallback.java
@@ -0,0 +1,170 @@
+//  Copyright 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.tapestry.callback;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IExternalPage;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  A callback for returning to an {@link org.apache.tapestry.IExternalPage}.
+ *  <p>
+ *  Example usage of <tt>ExternalCallback</tt>: 
+ *  <p>
+ *  The External page ensure a user is authenticated in the 
+ *  {@link org.apache.tapestry.IPage#validate(IRequestCycle)} method. 
+ *  If the user is not authenticated, they are redirected to the Login page, after 
+ *  setting a callback in the Login page.
+ *  <p>
+ *  The Login page <tt>formSubmit()</tt> {@link org.apache.tapestry.IActionListener} 
+ *  authenticates the user and then invokes {@link ICallback#performCallback(IRequestCycle)} 
+ *  to the External page.
+ *  <pre>
+ *  public class External extends BasePage implements IExternalPage {
+ * 
+ *      private Integer _itemId;
+ *
+ *      public void validate(IRequestCycle cycle) throws RequestCycleException {            
+ *          Visit visit = (Visit) getVisit();
+ *      
+ *          if (!visit.isAuthenticated()) {
+ *              Login login = (Login) cycle.getPage("Login");
+ *
+ *              login.setCallback
+ *                  (new ExternalCallback(this, cycle.getServiceParameters()));
+ *              
+ *              throw new PageRedirectException(login);
+ *          }            
+ *      }
+ * 
+ *      public void activateExternalPage(Object[] params, IRequestCycle cycle)
+ *              throws RequestCycleException {            
+ *          _itemId = (Integer) params[0];
+ *      }
+ *  }
+ *
+ *  public Login extends BasePage {
+ * 
+ *      private ICallback _callback;
+ *
+ *      public void setCallback(ICallback _callback) {
+ *          _callback = callback;
+ *      }
+ *
+ *      public void formSubmit(IRequestCycle cycle) {
+ *          // Authentication code
+ *          ..
+ *   
+ *          Visit visit = (Visit) getVisit();
+ *
+ *          visit.setAuthenticated(true);
+ *  
+ *          if (_callback != null) {
+ *              _callback.performCallback(cycle);
+ *          }
+ *      }
+ *  }    
+ *  </pre>
+ * 
+ *  @see org.apache.tapestry.IExternalPage
+ *  @see org.apache.tapestry.engine.ExternalService
+ *
+ *  @version $Id$
+ *  @author Malcolm Edgar
+ *  @since 2.3
+ *
+ **/
+
+public class ExternalCallback implements ICallback
+{
+    private String _pageName;
+    private Object[] _parameters;
+
+    /**
+     *  Creates a new ExternalCallback for the named <tt>IExternalPage</tt>.  
+     *  The parameters (which may be null) is retained, not copied.
+     *
+     **/
+
+    public ExternalCallback(String pageName, Object[] parameters)
+    {
+        _pageName = pageName;
+        _parameters = parameters;
+    }
+
+    /**
+     *  Creates a new ExternalCallback for the page.  The parameters
+     *  (which may be null) is retained, not copied.
+     *
+     **/
+
+    public ExternalCallback(IExternalPage page, Object[] parameters)
+    {
+        _pageName = page.getPageName();
+        _parameters = parameters;
+    }
+
+    /**
+     *  Invokes {@link IRequestCycle#setPage(String)} to select the previously
+     *  identified <tt>IExternalPage</tt> as the response page and activates
+     *  the page by invoking <tt>activateExternalPage()</tt> with the callback 
+     *  parameters and request cycle.
+     *
+     **/
+
+    public void performCallback(IRequestCycle cycle)
+    {        
+        try
+        {
+            IExternalPage page = (IExternalPage) cycle.getPage(_pageName);
+            
+            cycle.activate(page);
+    
+            page.activateExternalPage(_parameters, cycle);            
+        }
+        catch (ClassCastException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("ExternalCallback.page-not-compatible", _pageName),
+                ex);
+        }
+    }
+    
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer("ExternalCallback[");
+
+        buffer.append(_pageName);
+        buffer.append('/');
+
+        if (_parameters != null)
+        {
+            String sep = " ";
+
+            for (int i = 0; i < _parameters.length; i++)
+            {
+                buffer.append(sep);
+                buffer.append(_parameters[i]);
+
+                sep = ", ";
+            }
+        }
+
+        buffer.append(']');
+
+        return buffer.toString();
+    }    
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/callback/ICallback.java b/tapestry-framework/src/org/apache/tapestry/callback/ICallback.java
new file mode 100644
index 0000000..a6028ef
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/callback/ICallback.java
@@ -0,0 +1,46 @@
+//  Copyright 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.tapestry.callback;
+
+import java.io.Serializable;
+
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  Defines a callback, an object which is used to invoke or reinvoke a method
+ *  on an object or component in a later request cycle.  This is used to
+ *  allow certain operations (say, submitting an order) to defer to other processes
+ *  (say, logging in and/or registerring).
+ *
+ *  <p>Callbacks must be {@link Serializable}, to ensure that they can be stored
+ *  between request cycles.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *  @since 0.2.9
+ *
+ **/
+
+public interface ICallback extends Serializable
+{
+    /**
+     *  Performs the call back.  Typical implementation will locate a particular
+     *  page or component and invoke a method upon it, or 
+     *  invoke a method on the {@link IRequestCycle cycle}.
+     *
+     **/
+
+    public void performCallback(IRequestCycle cycle);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/callback/PageCallback.java b/tapestry-framework/src/org/apache/tapestry/callback/PageCallback.java
new file mode 100644
index 0000000..987c7bc
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/callback/PageCallback.java
@@ -0,0 +1,114 @@
+//  Copyright 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.tapestry.callback;
+
+import org.apache.tapestry.IPage;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  Simple callback for returning to a page.
+ *  <p>
+ *  Example usage of <tt>PageCallback</tt>:
+ *  <p>
+ *  The Home page ensure a user is 
+ *  authenticated in the {@link org.apache.tapestry.IPage#validate(IRequestCycle)} 
+ *  method.  If the user is not authenticated, they are redirected to the Login 
+ *  page, after setting a callback in the Login page.
+ *  <p>
+ *  The Login page <tt>formSubmit()</tt> {@link org.apache.tapestry.IActionListener} 
+ *  authenticates the user and then invokes {@link ICallback#performCallback(IRequestCycle)} 
+ *  to the Home page.
+ *  <pre>
+ *  public class Home extends BasePage {
+ * 
+ *      public void validate(IRequestCycle cycle) {            
+ *          Visit visit = (Visit) getVisit();
+ *      
+ *          if (!visit.isAuthenticated()) {
+ *              Login login = (Login) cycle.getPage("Login");
+ *
+ *              login.setCallback(new PageCallback(this));
+ *              
+ *              throw new PageRedirectException(login);
+ *          }            
+ *      }
+ *  }
+ *
+ *  public Login extends BasePage {
+ * 
+ *      private ICallback _callback;
+ *
+ *      public void setCallback(ICallback _callback) {
+ *          _callback = callback;
+ *      }
+ *
+ *      public void formSubmit(IRequestCycle cycle) {
+ *          // Authentication code
+ *          ..
+ *   
+ *          Visit visit = (Visit) getVisit();
+ *
+ *          visit.setAuthenticated(true);
+ *  
+ *          if (_callback != null) {
+ *              _callback.performCallback(cycle);
+ *          }
+ *      }
+ *  }    
+ *  </pre>
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *  @since 0.2.9
+ *
+ **/
+
+public class PageCallback implements ICallback
+{
+    /**
+     *  @since 2.0.4
+     * 
+     **/
+
+    private static final long serialVersionUID = -3286806776105690068L;
+
+    private String _pageName;
+
+    public PageCallback(String pageName)
+    {
+        _pageName = pageName;
+    }
+
+    public PageCallback(IPage page)
+    {
+        this(page.getPageName());
+    }
+
+    public String toString()
+    {
+        return "PageCallback[" + _pageName + "]";
+    }
+
+    /**
+     *  Invokes {@link IRequestCycle#activate(String)} to select the previously
+     *  identified page as the response page.
+     *
+     **/
+
+    public void performCallback(IRequestCycle cycle)
+    {
+        cycle.activate(_pageName);
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/callback/package.html b/tapestry-framework/src/org/apache/tapestry/callback/package.html
new file mode 100644
index 0000000..6623396
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/callback/package.html
@@ -0,0 +1,24 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+<p>Provides implementations of callbacks, objects that encapsulate a server request that is deferred,
+typically to allow a user to login or otherwise authenticate before proceeding with
+some other activity.
+
+<p>In practice, an implementation of {@link org.apache.tapestry.IPage#validate(IRequestCycle)} or
+{@link org.apache.tapestry.IActionListener} will create a callback, and assign it as a
+persistent page property of an application-specific login page.  After the login completes, it 
+can use the callback to return the user to the functionality that was deferred.
+
+<p>Another example use would be to collect billing and shipping information as part of
+an e-commerce site's checkout wizard.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/components/Any.java b/tapestry-framework/src/org/apache/tapestry/components/Any.java
new file mode 100644
index 0000000..00d1ede
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/components/Any.java
@@ -0,0 +1,63 @@
+//  Copyright 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.tapestry.components;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  A component that can substitute for any HTML element.  
+ *
+ *  [<a href="../../../../../ComponentReference/Any.html">Component Reference</a>]
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public abstract class Any extends AbstractComponent
+{
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        String element = getElement();
+
+        if (element == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("Any.element-not-defined"),
+                this,
+                null,
+                null);
+
+        if (!cycle.isRewinding())
+        {
+            writer.begin(element);
+
+            renderInformalParameters(writer, cycle);
+        }
+
+        renderBody(writer, cycle);
+
+        if (!cycle.isRewinding())
+        {
+            writer.end(element);
+        }
+
+    }
+
+    public abstract String getElement();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/components/Any.jwc b/tapestry-framework/src/org/apache/tapestry/components/Any.jwc
new file mode 100644
index 0000000..7807fbd
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/components/Any.jwc
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+<component-specification class="org.apache.tapestry.components.Any">
+
+  <description>
+  Dynamically emulates any element, including attributes (provided as
+  informal parameters).
+  </description>
+  
+  <parameter name="element" type="java.lang.String" direction="in" required="no" default-value="templateTag">
+  	<description>
+  	The element to emulate.
+  	</description>
+  </parameter>
+  
+  <parameter name="templateTag" type="java.lang.String" direction="auto" required="no" default-value="null">
+  	<description>
+  	The tag used to add this component in a template.
+  	</description>
+  </parameter>
+  
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/components/Block.java b/tapestry-framework/src/org/apache/tapestry/components/Block.java
new file mode 100644
index 0000000..b926828
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/components/Block.java
@@ -0,0 +1,73 @@
+//  Copyright 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.tapestry.components;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+
+/** 
+ *  Prevents its contents from being rendered until triggered by
+ *  an {@link RenderBlock} component.
+ *
+ *  [<a href="../../../../../ComponentReference/Block.html">Component Reference</a>]
+ *
+ *  <p>Block and {@link RenderBlock} are used to build a certain class
+ *  of complicated component that can't be assembled using the normal
+ *  wrapping containment.  Such a super component would have two or more
+ *  sections that need to be supplied by the containing page (or component).
+ *
+ *  <p>Using Blocks, the blocks can be provided as parameters to the super
+ *  component.
+ * 
+ *  <p>The inserter property gives the components inside the block access to
+ *  the component (typically an {@link RenderBlock}) that inserted the block,
+ *  including access to its informal bindings which allows components contained
+ *  by the Block to be passed parameters.  Note - it is the responsibility of the
+ *  inserting component to set itself as the Block's inserter.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *  @since 0.2.9
+ * 
+ **/
+
+public class Block extends AbstractComponent
+{
+	private IComponent _inserter;
+
+    /**
+     *  Does nothing; the idea of a Block is to defer the rendering of
+     *  the body of the block until an {@link RenderBlock} forces it
+     *  out.
+     *
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        // Nothing!
+    }
+    
+    public IComponent getInserter()
+    {
+    	return _inserter;
+    }
+    
+    public void setInserter(IComponent value)
+    {
+    	_inserter = value;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/components/Block.jwc b/tapestry-framework/src/org/apache/tapestry/components/Block.jwc
new file mode 100644
index 0000000..99323f9
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/components/Block.jwc
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.components.Block" allow-informal-parameters="no">
+	<description>
+	A block of dynamic content.  Blocks don't render until a RenderBlock component
+	triggers them.
+	</description>
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/components/BlockRenderer.java b/tapestry-framework/src/org/apache/tapestry/components/BlockRenderer.java
new file mode 100644
index 0000000..236c975
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/components/BlockRenderer.java
@@ -0,0 +1,79 @@
+//  Copyright 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.tapestry.components;
+
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ * An implementation of IRender that renders a Block component.
+ * 
+ * <p>The BlockRenderer allows the contents of a {@link Block} to be rendered
+ * via {@link IRender}. It can be used in cases when an {@link IRender} object is
+ * required as an argument or a binding to render a part of a Component. 
+ * To provide a complicated view, it could be defined in a {@link Block} and then
+ * returned encapsulated in a BlockRenderer.
+ * 
+ * <p>It is important to note that a special care has to be taken if 
+ * the BlockRenderer is used within an inner class of a component or a page. 
+ * In such a case the instance of the component that created the inner class 
+ * may not be the currently active instance in the RequestCycle when the 
+ * BlockRenderer is required. Thus, calling getComponent("blockName") to get the
+ * block component may return a Block component that is not initialized for this 
+ * RequestCycle.
+ * 
+ * <p>To avoid similar problems, the ComponentAddress class could be used in
+ * conjunction with BlockRenderer. 
+ * Here is a quick example of how BlockRenderer could be used with ComponentAddress:
+ * <p>
+ * <code>
+ * <br>// Create a component address for the current component
+ * <br>final ComponentAddress address = new ComponentAddress(this);
+ * <br>return new SomeClass() {
+ * <br>&nbsp;&nbsp;IRender getRenderer(IRequestCycle cycle) {
+ * <br>&nbsp;&nbsp;&nbsp;&nbsp;MyComponent component = (MyComponent) address.findComponent(cycle);
+ * <br>&nbsp;&nbsp;&nbsp;&nbsp;// initialize variables in the component that will be used by the block here
+ * <br>&nbsp;&nbsp;&nbsp;&nbsp;return new BlockRenderer(component.getComponent("block"));
+ * <br>&nbsp;&nbsp;}
+ * <br>}
+ * </code>
+ * 
+ * @version $Id$
+ * @author mindbridge
+ * @since 2.2
+ */
+public class BlockRenderer implements IRender
+{
+	private Block m_objBlock;
+
+	/**
+	 * Creates a new BlockRenderer that will render the content of the argument
+	 * @param objBlock the Block to be rendered
+	 */
+	public BlockRenderer(Block objBlock)
+	{
+		m_objBlock = objBlock;
+	}
+
+	/**
+	 * @see org.apache.tapestry.IRender#render(IMarkupWriter, IRequestCycle)
+	 */
+	public void render(IMarkupWriter writer, IRequestCycle cycle)
+	{
+		m_objBlock.renderBody(writer, cycle);
+	}
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/components/Conditional.java b/tapestry-framework/src/org/apache/tapestry/components/Conditional.java
new file mode 100644
index 0000000..9f59742
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/components/Conditional.java
@@ -0,0 +1,72 @@
+//  Copyright 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.tapestry.components;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  A conditional element on a page which will render its wrapped elements
+ *  zero or one times.
+ *
+ *  [<a href="../../../../../ComponentReference/Conditional.html">Component Reference</a>]
+ *
+ *  @author Howard Lewis Ship, David Solis
+ *  @version $Id$
+ * 
+ **/
+
+public abstract class Conditional extends AbstractComponent 
+{
+	/**
+	 *  Renders its wrapped components only if the condition is true (technically,
+	 *  if condition matches invert). 
+	 *  Additionally, if element is specified, can emulate that HTML element if condition is met
+	 *
+	 **/
+
+	protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle) 
+	{
+		if (evaluateCondition()) 
+		{
+			String element = getElement();
+			
+			boolean render = !cycle.isRewinding() && Tapestry.isNonBlank(element);
+			
+			if (render)
+			{
+				writer.begin(element);
+				renderInformalParameters(writer, cycle);
+			}
+
+			renderBody(writer, cycle);
+			
+			if (render)
+				writer.end(element);
+		}
+	}
+	
+	protected boolean evaluateCondition()
+	{
+		return getCondition() != getInvert();
+	}
+
+	public abstract boolean getCondition();
+	public abstract boolean getInvert();
+
+	public abstract String getElement();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/components/Conditional.jwc b/tapestry-framework/src/org/apache/tapestry/components/Conditional.jwc
new file mode 100644
index 0000000..cbff668
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/components/Conditional.jwc
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.components.Conditional">
+  <description>
+  Conditionally emulates an element and its attributes (if element is specified) and/or includes a block of content if a condition is met.
+  </description>
+  
+  <parameter name="condition" type="boolean" direction="in" required="yes">
+    <description>
+    The condition to evaluate.
+    </description>
+  </parameter>
+  
+  <parameter name="invert" type="boolean" direction="in">
+    <description>
+    If true, inverts the condition, so that a false condition causes the 
+    content to be included.  If false (the default), then the condition
+    is evaluated normally.
+    </description>
+  </parameter>
+
+  <parameter name="element" type="java.lang.String" direction="in" required="no">
+  	<description>
+  	The element to emulate.
+  	</description>
+  </parameter>
+  
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/components/Delegator.java b/tapestry-framework/src/org/apache/tapestry/components/Delegator.java
new file mode 100644
index 0000000..d817160
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/components/Delegator.java
@@ -0,0 +1,49 @@
+//  Copyright 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.tapestry.components;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  A component which delegates it's behavior to another object.
+ *
+ *  [<a href="../../../../../ComponentReference/Delegator.html">Component Reference</a>]
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public abstract class Delegator extends AbstractComponent
+{
+    /**
+     *  Gets its delegate and invokes {@link IRender#render(IMarkupWriter, IRequestCycle)}
+     *  on it.
+     *
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+    	IRender delegate = getDelegate();
+    	
+        if (delegate != null)
+        	delegate.render(writer, cycle);
+    }
+    
+    public abstract IRender getDelegate();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/components/Delegator.jwc b/tapestry-framework/src/org/apache/tapestry/components/Delegator.jwc
new file mode 100644
index 0000000..6000319
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/components/Delegator.jwc
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.components.Delegator"
+    allow-body="no" 
+	allow-informal-parameters="no">
+
+  <description>
+  Delegates rendering to an object that implements the IRender interface.
+  </description>
+
+  <parameter name="delegate" type="org.apache.tapestry.IRender" direction="in">
+    <description>
+    The object which will perform the render.
+    </description>
+  </parameter>
+  
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/components/Foreach.java b/tapestry-framework/src/org/apache/tapestry/components/Foreach.java
new file mode 100644
index 0000000..e43ef9d
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/components/Foreach.java
@@ -0,0 +1,174 @@
+//  Copyright 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.tapestry.components;
+
+import java.util.Iterator;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Repeatedly renders its wrapped contents while iterating through
+ *  a list of values.
+ *
+ *  [<a href="../../../../../ComponentReference/Foreach.html">Component Reference</a>]
+ *
+ *  <p>
+ *  While the component is rendering, the property
+ *  {@link #getValue() value} (accessed as
+ *  <code>components.<i>foreach</i>.value</code>
+ *  is set to each successive value from the source,
+ *  and the property
+ *  {@link #getIndex() index} is set to each successive index
+ *  into the source (starting with zero).
+ * 
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public abstract class Foreach extends AbstractComponent
+{
+    private Object _value;
+    private int _index;
+    private boolean _rendering;
+
+    public abstract IBinding getIndexBinding();
+
+
+    /**
+     *  Gets the source binding and returns an {@link Iterator}
+     *  representing
+     *  the values identified by the source.  Returns an empty {@link Iterator}
+     *  if the binding, or the binding value, is null.
+     *
+     *  <p>Invokes {@link Tapestry#coerceToIterator(Object)} to perform
+     *  the actual conversion.
+     *
+     **/
+
+    protected Iterator getSourceData()
+    {
+    	Object source = getSource();
+    	
+ 		if (source == null)
+ 			return null;
+ 		
+        return Tapestry.coerceToIterator(source);
+    }
+
+    public abstract IBinding getValueBinding();
+
+    /**
+     *  Gets the source binding and iterates through
+     *  its values.  For each, it updates the value binding and render's its wrapped elements.
+     *
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        Iterator dataSource = getSourceData();
+
+        // The dataSource was either not convertable, or was empty.
+
+        if (dataSource == null)
+            return;
+
+
+        try
+        {
+            _rendering = true;
+            _value = null;
+            _index = 0;
+            
+            IBinding indexBinding = getIndexBinding();
+            IBinding valueBinding = getValueBinding();
+            String element = getElement();
+
+            boolean hasNext = dataSource.hasNext();
+
+            while (hasNext)
+            {
+                _value = dataSource.next();
+                hasNext = dataSource.hasNext();
+
+                if (indexBinding != null)
+                    indexBinding.setInt(_index);
+
+                if (valueBinding != null)
+                    valueBinding.setObject(_value);
+
+                if (element != null)
+                {
+                    writer.begin(element);
+                    renderInformalParameters(writer, cycle);
+                }
+
+                renderBody(writer, cycle);
+
+                if (element != null)
+                    writer.end();
+
+                _index++;
+            }
+        }
+        finally
+        {
+            _value = null;
+            _rendering = false;
+        }
+    }
+
+    /**
+     *  Returns the most recent value extracted from the source parameter.
+     *
+     *  @throws org.apache.tapestry.ApplicationRuntimeException if the Foreach is not currently rendering.
+     *
+     **/
+
+    public Object getValue()
+    {
+        if (!_rendering)
+            throw Tapestry.createRenderOnlyPropertyException(this, "value");
+  
+        return _value;
+    }
+
+    public abstract String getElement();
+
+    public abstract Object getSource();
+
+    /**
+     *  The index number, within the {@link #getSource() source}, of the
+     *  the current value.
+     * 
+     *  @throws org.apache.tapestry.ApplicationRuntimeException if the Foreach is not currently rendering.
+     *
+     *  @since 2.2
+     * 
+     **/
+    
+    public int getIndex()
+    {
+        if (!_rendering)
+            throw Tapestry.createRenderOnlyPropertyException(this, "index");
+        
+        return _index;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/components/Foreach.jwc b/tapestry-framework/src/org/apache/tapestry/components/Foreach.jwc
new file mode 100644
index 0000000..1e5ca90
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/components/Foreach.jwc
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification 
+	class="org.apache.tapestry.components.Foreach" 
+	allow-informal-parameters="yes">
+	
+  <description>
+  Loops over a collection of source values.  May also emulate an element (like an Any
+  component).
+  </description>
+	
+  <parameter name="source" type="java.lang.Object" direction="in" required="yes">
+    <description>
+    The source of values, a Java collection or array.
+    </description>
+  </parameter>
+  
+  <parameter name="value" direction="custom">
+    <description>
+    If provided, then on each iteration, the value property is updated.
+    </description>
+  </parameter>
+  
+  <parameter name="index" type="int" direction="custom">
+    <description>
+    If provided, then the index of the loop is set on each iteration.
+    </description>
+  </parameter>
+  
+  <parameter name="element" type="java.lang.String" direction="in">
+    <description>
+    If provided, then the Foreach creates an element wrapping its content.
+    Informal parameters become attributes of the element.
+    </description>
+  </parameter>
+  
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/components/ILinkComponent.java b/tapestry-framework/src/org/apache/tapestry/components/ILinkComponent.java
new file mode 100644
index 0000000..fae2537
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/components/ILinkComponent.java
@@ -0,0 +1,89 @@
+//  Copyright 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.tapestry.components;
+
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.engine.ILink;
+
+/**
+ *  A component that renders an HTML &lt;a&gt; element.  It exposes some
+ *  properties to the components it wraps.  This is basically to facilitate
+ *  the {@link org.apache.tapestry.html.Rollover} component.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public interface ILinkComponent extends IComponent
+{
+
+    /**
+     *  Returns whether this service link component is enabled or disabled.
+     *
+     *  @since 0.2.9
+     *
+     **/
+
+    public boolean isDisabled();
+
+    /**
+     *  Returns the anchor defined for this link, or null for no anchor.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    public String getAnchor();
+
+    /**
+     *  Adds a new event handler.  When the event occurs, the JavaScript function
+     *  specified is executed.  Multiple functions can be specified, in which case
+     *  all of them are executed.
+     *
+     *  <p>This was created for use by
+     *  {@link org.apache.tapestry.html.Rollover} to set mouse over and mouse out handlers on
+     *  the {@link ILinkComponent} that wraps it, but can be used for
+     *  many other things as well.
+     *
+     *  @since 0.2.9
+     **/
+
+    public void addEventHandler(LinkEventType type, String functionName);
+
+    /**
+     *  Invoked by the {@link org.apache.tapestry.link.ILinkRenderer} (if
+     *  the link is not disabled) to provide a
+     *  {@link org.apache.tapestry.engine.EngineServiceLink} that the renderer can convert
+     *  into a URL.
+     * 
+     **/
+
+    public ILink getLink(IRequestCycle cycle);
+
+    /**
+     *  Invoked (by the {@link org.apache.tapestry.link.ILinkRenderer})
+     *  to make the link render any additional attributes.  These
+     *  are informal parameters, plus any attributes related to events.
+     *  This is only invoked for non-disabled links.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    public void renderAdditionalAttributes(IMarkupWriter writer, IRequestCycle cycle);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/components/Insert.java b/tapestry-framework/src/org/apache/tapestry/components/Insert.java
new file mode 100644
index 0000000..3820819
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/components/Insert.java
@@ -0,0 +1,106 @@
+//  Copyright 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.tapestry.components;
+
+import java.text.Format;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Used to insert some text (from a parameter) into the HTML.
+ *
+ *  [<a href="../../../../../ComponentReference/Insert.html">Component Reference</a>]
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public abstract class Insert extends AbstractComponent
+{
+    public abstract IBinding getFormatBinding();
+
+    /**
+     *  Prints its value parameter, possibly formatted by its format parameter.
+     *
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        if (cycle.isRewinding())
+            return;
+
+        Object value = getValue();
+
+        if (value == null)
+            return;
+
+        String insert = null;
+
+        Format format = getFormat();
+
+        if (format == null)
+        {
+            insert = value.toString();
+        }
+        else
+        {
+            try
+            {
+                insert = format.format(value);
+            }
+            catch (Exception ex)
+            {
+                throw new ApplicationRuntimeException(
+                    Tapestry.format("Insert.unable-to-format", value),
+                    this,
+                    getFormatBinding().getLocation(),
+                    ex);
+            }
+        }
+
+        String styleClass = getStyleClass();
+
+        if (styleClass != null)
+        {
+            writer.begin("span");
+            writer.attribute("class", styleClass);
+
+            renderInformalParameters(writer, cycle);
+        }
+
+        if (getRaw())
+            writer.printRaw(insert);
+        else
+            writer.print(insert);
+
+        if (styleClass != null)
+            writer.end(); // <span>
+    }
+
+    public abstract Object getValue();
+
+    public abstract Format getFormat();
+
+    public abstract String getStyleClass();
+
+    public abstract boolean getRaw();
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/components/Insert.jwc b/tapestry-framework/src/org/apache/tapestry/components/Insert.jwc
new file mode 100644
index 0000000..1cb5409
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/components/Insert.jwc
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.components.Insert" 
+	allow-body="no">
+  
+  <description>
+  Emits a value into the response page.
+  </description>
+  
+  
+  <parameter name="value" type="java.lang.Object" direction="in">
+  	<description>
+  	The value to be emitted.  Non-strings are converted to strings.
+  	</description>
+  </parameter>
+  
+  <parameter name="format" type="java.text.Format" direction="in">
+  	<description>
+  	A Format object used to convert the value to a string.
+  	</description>
+  </parameter>
+  
+  <parameter name="raw" type="boolean" direction="in">
+  	<description>
+  	If false (the default), then HTML characters in the value are escaped.  If
+  	true, then value is emitted exactly as is.
+  	</description>
+  </parameter>
+  
+  <parameter name="class" 
+  	type="java.lang.String"
+  	property-name="styleClass"
+  	direction="in">
+  	<description>
+  	If specified, then any output is wrapped in an HTML span tag with the given CSS class.
+  	</description>
+  </parameter>
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/components/LinkEventType.java b/tapestry-framework/src/org/apache/tapestry/components/LinkEventType.java
new file mode 100644
index 0000000..7682dd6
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/components/LinkEventType.java
@@ -0,0 +1,110 @@
+//  Copyright 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.tapestry.components;
+
+import org.apache.commons.lang.enum.Enum;
+
+/**
+ *  Different types of JavaScript events that an {@link ILinkComponent}
+ *  can provide handlers for.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 0.2.9
+ *
+ **/
+
+public class LinkEventType extends Enum
+{
+    private String _attributeName;
+
+    /**
+     *  Type for <code>onMouseOver</code>.  This may also be called "focus".
+     *
+     **/
+
+    public static final LinkEventType MOUSE_OVER = new LinkEventType("MOUSE_OVER", "onMouseOver");
+
+    /**
+     * Type for <code>onMouseOut</code>.  This may also be called "blur".
+     *
+     **/
+
+    public static final LinkEventType MOUSE_OUT = new LinkEventType("MOUSE_OUT", "onMouseOut");
+
+    /**
+     * Type for <code>onClick</code>.
+     *
+     * @since 1.0.1
+     *
+     **/
+
+    public static final LinkEventType CLICK = new LinkEventType("CLICK", "onClick");
+
+    /**
+     * Type for <code>onDblClick</code>.
+     *
+     * @since 1.0.1
+     *
+     **/
+
+    public static final LinkEventType DOUBLE_CLICK =
+        new LinkEventType("DOUBLE_CLICK", "onDblClick");
+
+    /**
+     * Type for <code>onMouseDown</code>.
+     *
+     * @since 1.0.1.
+     *
+     **/
+
+    public static final LinkEventType MOUSE_DOWN = new LinkEventType("MOUSE_DOWN", "onMouseDown");
+
+    /**
+     * Type for <code>onMouseUp</code>.
+     *
+     * @since 1.0.1
+     *
+     **/
+
+    public static final LinkEventType MOUSE_UP = new LinkEventType("MOUSE_UP", "onMouseUp");
+
+    /**
+     *  Constructs a new type of event.  The name should match the
+     *  static final variable (i.e., MOUSE_OVER) and the attributeName
+     *  is the name of the HTML attribute to be managed (i.e., "onMouseOver").
+     *
+     *  <p>This method is protected so that subclasses can be created
+     *  to provide additional managed event types.
+     **/
+
+    protected LinkEventType(String name, String attributeName)
+    {
+        super(name);
+
+        _attributeName = attributeName;
+    }
+
+    /**
+     *  Returns the name of the HTML attribute corresponding to this
+     *  type.
+     *
+     **/
+
+    public String getAttributeName()
+    {
+        return _attributeName;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/components/RenderBlock.java b/tapestry-framework/src/org/apache/tapestry/components/RenderBlock.java
new file mode 100644
index 0000000..9d08eac
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/components/RenderBlock.java
@@ -0,0 +1,80 @@
+//  Copyright 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.tapestry.components;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  Renders the text and components wrapped by a {@link Block} component.
+ *
+ *  [<a href="../../../../../ComponentReference/RenderBlock.html">Component Reference</a>]
+ *
+ *  <p>It is possible for an RenderBlock to obtain a Block
+ *  from a page <em>other than</em> the render page.  This works, even when
+ *  the Block contains links, forms and form components.  The action and
+ *  direct services will create URLs that properly address this situation.
+ *
+ *  <p>However, because the rendering page can't know
+ *  ahead of time about these foriegn Blocks,
+ *  {@link org.apache.tapestry.event.PageRenderListener} methods
+ *  (for components and objects of the foriegn page)
+ *  via RenderBlock will <em>not</em> be executed.  This specifically
+ *  affects the methods of the {@link org.apache.tapestry.event.PageRenderListener} 
+ *  interface.
+ * 
+ *  <p>Before rendering its {@link Block}, RenderBlock will set itself as the 
+ *  Block's inserter, and will reset the inserter after the {@link Block} is 
+ *  rendered.  This gives the components contained in the {@link Block} access
+ *  to its inserted environment via the RenderBlock.  In particular this allows
+ *  the contained components to access the informal parameters of the RenderBlock
+ *  which effectively allows parameters to be passed to the components contained
+ *  in a Block.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * 
+ **/
+
+public abstract class RenderBlock extends AbstractComponent
+{
+    /**
+     *  If block is not null,
+     *  then the block's inserter is set (to this),
+     * {@link org.apache.tapestry.IComponent#renderBody(IMarkupWriter, IRequestCycle)}
+     *  is invoked on it, and the Block's inserter is set back to its previous state.
+     *
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+    	Block block = getBlock();
+    	
+        if (block != null)
+        {
+            // make a copy of the inserter so we don't overwrite completely
+            IComponent previousInserter = block.getInserter();
+            block.setInserter(this);
+            block.renderBody(writer, cycle);
+            // reset the inserter as it was before we changed it
+            block.setInserter(previousInserter);
+        }
+    }
+
+    public abstract Block getBlock();
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/components/RenderBlock.jwc b/tapestry-framework/src/org/apache/tapestry/components/RenderBlock.jwc
new file mode 100644
index 0000000..7c384d7
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/components/RenderBlock.jwc
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.components.RenderBlock" 
+	allow-body="no" 
+	allow-informal-parameters="yes">
+	
+  <parameter name="block" 
+  	type="org.apache.tapestry.components.Block"
+  	direction="in"/>
+  	
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/components/RenderBody.java b/tapestry-framework/src/org/apache/tapestry/components/RenderBody.java
new file mode 100644
index 0000000..00af581
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/components/RenderBody.java
@@ -0,0 +1,47 @@
+//  Copyright 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.tapestry.components;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  Renders the text and components wrapped by a component.
+ *
+ *  [<a href="../../../../../ComponentReference/RenderBody.html">Component Reference</a>]
+ * 
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class RenderBody extends AbstractComponent
+{
+    /**
+     *  Finds this <code>RenderBody</code>'s container, and invokes
+     *  {@link IComponent#renderBody(IMarkupWriter, IRequestCycle)}
+     *  on it.
+     *
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        IComponent container = getContainer();
+
+        container.renderBody(writer, cycle);
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/components/RenderBody.jwc b/tapestry-framework/src/org/apache/tapestry/components/RenderBody.jwc
new file mode 100644
index 0000000..e21fd48
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/components/RenderBody.jwc
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification 
+	class="org.apache.tapestry.components.RenderBody" 
+	allow-body="no" 
+	allow-informal-parameters="no"/>
diff --git a/tapestry-framework/src/org/apache/tapestry/components/package.html b/tapestry-framework/src/org/apache/tapestry/components/package.html
new file mode 100644
index 0000000..3ccd00e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/components/package.html
@@ -0,0 +1,15 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+<p>Basic, fundamental components used to construct more complex components, or pages.  
+These components are independant of any particular markup language.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/AbstractEngine.java b/tapestry-framework/src/org/apache/tapestry/engine/AbstractEngine.java
new file mode 100644
index 0000000..471b657
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/AbstractEngine.java
@@ -0,0 +1,2369 @@
+//  Copyright 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.tapestry.engine;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.ResourceBundle;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpSessionBindingEvent;
+import javax.servlet.http.HttpSessionBindingListener;
+
+import org.apache.bsf.BSFManager;
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.ApplicationServlet;
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.INamespace;
+import org.apache.tapestry.IPage;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.PageRedirectException;
+import org.apache.tapestry.RedirectException;
+import org.apache.tapestry.StaleLinkException;
+import org.apache.tapestry.StaleSessionException;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.asset.ResourceChecksumSource;
+import org.apache.tapestry.asset.ResourceChecksumSourceImpl;
+import org.apache.tapestry.enhance.DefaultComponentClassEnhancer;
+import org.apache.tapestry.listener.ListenerMap;
+import org.apache.tapestry.pageload.PageSource;
+import org.apache.tapestry.request.RequestContext;
+import org.apache.tapestry.request.ResponseOutputStream;
+import org.apache.tapestry.spec.IApplicationSpecification;
+import org.apache.tapestry.util.DelegatingPropertySource;
+import org.apache.tapestry.util.PropertyHolderPropertySource;
+import org.apache.tapestry.util.ResourceBundlePropertySource;
+import org.apache.tapestry.util.ServletContextPropertySource;
+import org.apache.tapestry.util.ServletPropertySource;
+import org.apache.tapestry.util.SystemPropertiesPropertySource;
+import org.apache.tapestry.util.exception.ExceptionAnalyzer;
+import org.apache.tapestry.util.io.DataSqueezer;
+import org.apache.tapestry.util.pool.Pool;
+
+/**
+ *  Basis for building real Tapestry applications.  Immediate subclasses
+ *  provide different strategies for managing page state and other resources
+ *  between request cycles.
+ *
+ *  Uses a shared instance of
+ *  {@link ITemplateSource}, {@link ISpecificationSource},
+ *  {@link IScriptSource} and {@link IComponentMessagesSource}
+ *  stored as attributes of the  {@link ServletContext}
+ *  (they will be shared by all sessions).
+ *
+ *  <p>An application is designed to be very lightweight.
+ *  Particularily, it should <b>never</b> hold references to any
+ *  {@link IPage} or {@link org.apache.tapestry.IComponent} objects.  The entire system is
+ *  based upon being able to quickly rebuild the state of any page(s).
+ *
+ * <p>Where possible, instance variables should be transient.  They
+ * can be restored inside {@link #setupForRequest(RequestContext)}.
+ *
+ *  <p>In practice, a subclass (usually {@link BaseEngine})
+ *  is used without subclassing.  Instead, a
+ *  visit object is specified.  To facilitate this, the application specification
+ *  may include a property, <code>org.apache.tapestry.visit-class</code>
+ *  which is the class name  to instantiate when a visit object is first needed.  See
+ *  {@link #createVisit(IRequestCycle)} for more details.
+ *
+ * <p>Some of the classes' behavior is controlled by JVM system properties
+ * (typically only used during development):
+ *
+ * <table border=1>
+ * 	<tr> <th>Property</th> <th>Description</th> </tr>
+ *  <tr> <td>org.apache.tapestry.enable-reset-service</td>
+ *		<td>If true, enabled an additional service, reset, that
+ *		allow page, specification and template caches to be cleared on demand.
+ *  	See {@link #isResetServiceEnabled()}. </td>
+ * </tr>
+ * <tr>
+ *		<td>org.apache.tapestry.disable-caching</td>
+ *	<td>If true, then the page, specification, template and script caches
+ *  will be cleared after each request. This slows things down,
+ *  but ensures that the latest versions of such files are used.
+ *  Care should be taken that the source directories for the files
+ *  preceeds any versions of the files available in JARs or WARs. </td>
+ * </tr>
+ * </table>
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+public abstract class AbstractEngine
+    implements IEngine, IEngineServiceView, Externalizable, HttpSessionBindingListener
+{
+    private static final Log LOG = LogFactory.getLog(AbstractEngine.class);
+
+    /**
+     *  @since 2.0.4
+     *
+     **/
+
+    private static final long serialVersionUID = 6884834397673817117L;
+
+    private transient String _contextPath;
+    private transient String _servletPath;
+    private transient String _clientAddress;
+    private transient String _sessionId;
+    private transient boolean _stateful;
+    private transient ListenerMap _listeners;
+
+    /** @since 2.2 **/
+
+    private transient DataSqueezer _dataSqueezer;
+
+    /**
+     *  An object used to contain application-specific server side state.
+     *
+     **/
+
+    private Object _visit;
+
+    /**
+     *  The globally shared application object.  Typically, this is created
+     *  when first needed, shared between sessions and engines, and
+     *  stored in the {@link ServletContext}.
+     *
+     *  @since 2.3
+     *
+     **/
+
+    private transient Object _global;
+
+    /**
+     *  The base name for the servlet context key used to store
+     *  the application-defined Global object, if any.
+     *
+     *  @since 2.3
+     *
+     **/
+
+    public static final String GLOBAL_NAME = "org.apache.tapestry.global";
+
+    /**
+     *  The name of the application property that will be used to
+     *  determine the encoding to use when generating the output
+     *
+     *  @since 3.0
+     **/
+
+    public static final String OUTPUT_ENCODING_PROPERTY_NAME =
+        "org.apache.tapestry.output-encoding";
+
+    /**
+     *  The default encoding that will be used when generating the output.
+     *  It is used if no output encoding property has been specified.
+     *
+     *  @since 3.0
+     */
+
+    public static final String DEFAULT_OUTPUT_ENCODING = "UTF-8";
+
+    /**
+     *  The curent locale for the engine, which may be changed at any time.
+     *
+     **/
+
+    private Locale _locale;
+
+    /**
+     *  Set by {@link #setLocale(Locale)} when the locale is changed;
+     *  this allows the locale cookie to be updated.
+     *
+     **/
+
+    private boolean _localeChanged;
+
+    /**
+     *  The specification for the application, which
+     *  lives in the {@link ServletContext}.  If the
+     *  session (and application) moves to a different context (i.e.,
+     *  a different JVM), then
+     *  we want to reconnect to the specification in the new context.
+     *  A check is made on every request
+     *  cycle as needed.
+     *
+     **/
+
+    protected transient IApplicationSpecification _specification;
+
+    /**
+     *  The source for template data. The template source is stored
+     *  in the {@link ServletContext} as a named attribute.
+     *  After de-serialization, the application can re-connect to
+     *  the template source (or create a new one).
+     *
+     **/
+
+    protected transient ITemplateSource _templateSource;
+
+    /**
+     *  The source for component specifications, stored in the
+     *  {@link ServletContext} (like {@link #_templateSource}).
+     *
+     **/
+
+    protected transient ISpecificationSource _specificationSource;
+
+    /**
+     *  The source for parsed scripts, again, stored in the
+     *  {@link ServletContext}.
+     *
+     *  @since 1.0.2
+     *
+     **/
+
+    private transient IScriptSource _scriptSource;
+
+    /**
+     *  The name of the context attribute for the {@link IScriptSource} instance.
+     *  The application's name is appended.
+     *
+     *  @since 1.0.2
+     *
+     **/
+
+    protected static final String SCRIPT_SOURCE_NAME = "org.apache.tapestry.ScriptSource";
+
+    /**
+     *  The name of the context attribute for the {@link IComponentMessagesSource}
+     *  instance.  The application's name is appended.
+     *
+     *  @since 2.0.4
+     *
+     **/
+
+    protected static final String STRINGS_SOURCE_NAME = "org.apache.tapestry.StringsSource";
+
+    private transient IComponentMessagesSource _stringsSource;
+
+    /**
+     *  The name of the application specification property used to specify the
+     *  class of the visit object.
+     *
+     **/
+
+    public static final String VISIT_CLASS_PROPERTY_NAME = "org.apache.tapestry.visit-class";
+
+    /**
+     *  Servlet context attribute name for the default {@link ITemplateSource}
+     *  instance.  The application's name is appended.
+     *
+     **/
+
+    protected static final String TEMPLATE_SOURCE_NAME = "org.apache.tapestry.TemplateSource";
+
+    /**
+     *  Servlet context attribute name for the default {@link ISpecificationSource}
+     *  instance.  The application's name is appended.
+     *
+     **/
+
+    protected static final String SPECIFICATION_SOURCE_NAME =
+        "org.apache.tapestry.SpecificationSource";
+
+    /**
+     *  Servlet context attribute name for the {@link IPageSource}
+     *  instance.  The application's name is appended.
+     *
+     **/
+
+    protected static final String PAGE_SOURCE_NAME = "org.apache.tapestry.PageSource";
+
+    /**
+     *  Servlet context attribute name for a shared instance
+     *  of {@link DataSqueezer}.  The instance is actually shared
+     *  between Tapestry applications within the same context
+     *  (which will have the same ClassLoader).
+     *
+     *  @since 2.2
+     *
+     **/
+
+    protected static final String DATA_SQUEEZER_NAME = "org.apache.tapestry.DataSqueezer";
+
+    /**
+     * Servlet context attribute name for a shared instance
+     * of {@link ResourceChecksumSource}.
+     * @since 3.0.3
+     */
+    protected static final String RESOURCE_CHECKSUM_SOURCE_NAME = 
+        "org.apache.tapestry.ResourceChecksumSource";
+    
+    /**
+     *  The source for pages, which acts as a pool, but is capable of
+     *  creating pages as needed.  Stored in the
+     *  {@link ServletContext}, like {@link #_templateSource}.
+     *
+     **/
+
+    private transient IPageSource _pageSource;
+
+    /**
+     *  If true (set from JVM system parameter
+     *  <code>org.apache.tapestry.enable-reset-service</code>)
+     *  then the reset service will be enabled, allowing
+     *  the cache of pages, specifications and template
+     *  to be cleared on demand.
+     *
+     **/
+
+    private static final boolean _resetServiceEnabled =
+        Boolean.getBoolean("org.apache.tapestry.enable-reset-service");
+
+    /**
+     * If true (set from the JVM system parameter
+     * <code>org.apache.tapestry.disable-caching</code>)
+     * then the cache of pages, specifications and template
+     * will be cleared after each request.
+     *
+     **/
+
+    private static final boolean _disableCaching =
+        Boolean.getBoolean("org.apache.tapestry.disable-caching");
+
+    private transient IResourceResolver _resolver;
+
+    /**
+     *  Constant used to store a {@link org.apache.tapestry.util.IPropertyHolder}
+     *  in the servlet context.
+     *
+     *  @since 2.3
+     *
+     **/
+
+    protected static final String PROPERTY_SOURCE_NAME = "org.apache.tapestry.PropertySource";
+
+    /**
+     *  A shared instance of {@link IPropertySource}
+     *
+     *  @since 3.0
+     *  @see #createPropertySource(RequestContext)
+     *
+     **/
+
+    private transient IPropertySource _propertySource;
+
+    /**
+     *  Map from service name to service instance.
+     *
+     *  @since 1.0.9
+     *
+     **/
+
+    private transient Map _serviceMap;
+
+    protected static final String SERVICE_MAP_NAME = "org.apache.tapestry.ServiceMap";
+
+    /**
+     *  A shared instance of {@link Pool}.
+     *
+     *  @since 3.0
+     *  @see #createPool(RequestContext)
+     *
+     **/
+
+    private transient Pool _pool;
+
+    protected static final String POOL_NAME = "org.apache.tapestry.Pool";
+
+    /**
+     *  Name of a shared instance of {@link org.apache.tapestry.engine.IComponentClassEnhancer}
+     *  stored in the {@link ServletContext}.
+     *
+     *  @since 3.0
+     *
+     **/
+
+    protected static final String ENHANCER_NAME = "org.apache.tapestry.ComponentClassEnhancer";
+
+    /**
+     *  A shared instance of {@link org.apache.tapestry.engine.IComponentClassEnhancer}.
+     *
+     *  @since 3.0
+     *  @see #createComponentClassEnhancer(RequestContext)
+     *
+     **/
+
+    private transient IComponentClassEnhancer _enhancer;
+
+    /**
+     *  Set to true when there is a (potential)
+     *  change to the internal state of the engine, set
+     *  to false when the engine is stored into the
+     *  {@link HttpSession}.
+     *
+     *  @since 3.0
+     *
+     **/
+
+    private transient boolean _dirty;
+
+    /**
+     * The instance of {@link IMonitorFactory} used to create a monitor.
+     *
+     * @since 3.0
+     */
+
+    private transient IMonitorFactory _monitorFactory;
+
+    /**
+     * Used to obtain resource checksums for the asset service.
+     * @since 3.0.3
+     */
+    private transient ResourceChecksumSource _resourceChecksumSource;
+    
+    /**
+     *  Sets the Exception page's exception property, then renders the Exception page.
+     *
+     *  <p>If the render throws an exception, then copious output is sent to
+     *  <code>System.err</code> and a {@link ServletException} is thrown.
+     *
+     **/
+
+    protected void activateExceptionPage(
+        IRequestCycle cycle,
+        ResponseOutputStream output,
+        Throwable cause)
+        throws ServletException
+    {
+        try
+        {
+            IPage exceptionPage = cycle.getPage(getExceptionPageName());
+
+            exceptionPage.setProperty("exception", cause);
+
+            cycle.activate(exceptionPage);
+
+            renderResponse(cycle, output);
+
+        }
+        catch (Throwable ex)
+        {
+            // Worst case scenario.  The exception page itself is broken, leaving
+            // us with no option but to write the cause to the output.
+
+            reportException(
+                Tapestry.getMessage("AbstractEngine.unable-to-process-client-request"),
+                cause);
+
+            // Also, write the exception thrown when redendering the exception
+            // page, so that can get fixed as well.
+
+            reportException(
+                Tapestry.getMessage("AbstractEngine.unable-to-present-exception-page"),
+                ex);
+
+            // And throw the exception.
+
+            throw new ServletException(ex.getMessage(), ex);
+        }
+    }
+
+    /**
+     *  Writes a detailed report of the exception to <code>System.err</code>.
+     *
+     **/
+
+    public void reportException(String reportTitle, Throwable ex)
+    {
+        LOG.warn(reportTitle, ex);
+
+        System.err.println("\n\n**********************************************************\n\n");
+
+        System.err.println(reportTitle);
+
+        System.err.println(
+            "\n\n      Session id: "
+                + _sessionId
+                + "\n  Client address: "
+                + _clientAddress
+                + "\n\nExceptions:\n");
+
+        new ExceptionAnalyzer().reportException(ex, System.err);
+
+        System.err.println("\n**********************************************************\n");
+
+    }
+
+    /**
+     *  Invoked at the end of the request cycle to release any resources specific
+     *  to the request cycle.
+     *
+     **/
+
+    protected abstract void cleanupAfterRequest(IRequestCycle cycle);
+
+    /**
+     *  Extends the description of the class generated by {@link #toString()}.
+     *  If a subclass adds additional instance variables that should be described
+     *  in the instance description, it may overide this method. This implementation
+     *  does nothing.
+     *
+     *  @see #toString()
+     *
+     **/
+
+    protected void extendDescription(ToStringBuilder builder)
+    {
+
+    }
+
+    /**
+     *  Returns the locale for the engine.  This is initially set
+     *  by the {@link ApplicationServlet} but may be updated
+     *  by the application.
+     *
+     **/
+
+    public Locale getLocale()
+    {
+        return _locale;
+    }
+
+    /**
+     * Overriden in subclasses that support monitoring.  Should create and return
+     * an instance of {@link IMonitor} that is appropriate for the request cycle described
+     * by the {@link RequestContext}.
+     *
+     * <p>The monitor is used to create a {@link RequestCycle}.
+     *
+     * <p>This implementation uses a {@link IMonitorFactory}
+     * to create the monitor instance.  The factory
+     * is provided as an application extension.  If the application
+     * extension does not exist, {@link DefaultMonitorFactory} is used.
+     *
+     * <p>As of release 3.0, this method should <em>not</em> return null.
+     *
+     *
+     */
+
+    public IMonitor getMonitor(RequestContext context)
+    {
+        if (_monitorFactory == null)
+        {
+            if (_specification.checkExtension(Tapestry.MONITOR_FACTORY_EXTENSION_NAME))
+                _monitorFactory =
+                    (IMonitorFactory) _specification.getExtension(
+                        Tapestry.MONITOR_FACTORY_EXTENSION_NAME,
+                        IMonitorFactory.class);
+            else
+                _monitorFactory = DefaultMonitorFactory.SHARED;
+        }
+
+        return _monitorFactory.createMonitor(context);
+    }
+
+    public IPageSource getPageSource()
+    {
+        return _pageSource;
+    }
+
+    /**
+     *  Returns a service with the given name.  Services are created by the
+     *  first call to {@link #setupForRequest(RequestContext)}.
+     **/
+
+    public IEngineService getService(String name)
+    {
+        IEngineService result = (IEngineService) _serviceMap.get(name);
+
+        if (result == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.format("AbstractEngine.unknown-service", name));
+
+        return result;
+    }
+
+    public String getServletPath()
+    {
+        return _servletPath;
+    }
+
+    /**
+     * Returns the context path, the prefix to apply to any URLs so that they
+     * are recognized as belonging to the Servlet 2.2 context.
+     *
+     *  @see org.apache.tapestry.asset.ContextAsset
+     *
+     **/
+
+    public String getContextPath()
+    {
+        return _contextPath;
+    }
+
+    /**
+     *  Returns the specification, if available, or null otherwise.
+     *
+     *  <p>To facilitate deployment across multiple servlet containers, the
+     *  application is serializable.  However, the reference to the specification
+     *  is transient.   When an application instance is deserialized, it reconnects
+     *  with the application specification by locating it in the {@link ServletContext}
+     *  or parsing it fresh.
+     *
+     **/
+
+    public IApplicationSpecification getSpecification()
+    {
+        return _specification;
+    }
+
+    public ISpecificationSource getSpecificationSource()
+    {
+        return _specificationSource;
+    }
+
+    public ITemplateSource getTemplateSource()
+    {
+        return _templateSource;
+    }
+
+    /**
+     *  Reads the state serialized by {@link #writeExternal(ObjectOutput)}.
+     *
+     *  <p>This always set the stateful flag.  By default, a deserialized
+     *  session is stateful (else, it would not have been serialized).
+     **/
+
+    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
+    {
+        _stateful = true;
+
+        String localeName = in.readUTF();
+        _locale = Tapestry.getLocale(localeName);
+
+        _visit = in.readObject();
+    }
+
+    /**
+     *  Writes the following properties:
+     *
+     *  <ul>
+     *  <li>locale name ({@link Locale#toString()})
+     *  <li>visit
+     *  </ul>
+     *
+     **/
+
+    public void writeExternal(ObjectOutput out) throws IOException
+    {
+        out.writeUTF(_locale.toString());
+        out.writeObject(_visit);
+    }
+
+    /**
+     *  Invoked, typically, when an exception occurs while servicing the request.
+     *  This method resets the output, sets the new page and renders it.
+     *
+     **/
+
+    protected void redirect(
+        String pageName,
+        IRequestCycle cycle,
+        ResponseOutputStream out,
+        ApplicationRuntimeException exception)
+        throws IOException, ServletException
+    {
+        // Discard any output from the previous page.
+
+        out.reset();
+
+        IPage page = cycle.getPage(pageName);
+
+        cycle.activate(page);
+
+        renderResponse(cycle, out);
+    }
+
+    public void renderResponse(IRequestCycle cycle, ResponseOutputStream output)
+        throws ServletException, IOException
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Begin render response.");
+
+        // If the locale has changed during this request cycle then
+        // do the work to propogate the locale change into
+        // subsequent request cycles.
+
+        if (_localeChanged)
+        {
+            _localeChanged = false;
+
+            RequestContext context = cycle.getRequestContext();
+            ApplicationServlet servlet = context.getServlet();
+
+            servlet.writeLocaleCookie(_locale, this, context);
+        }
+
+        // Commit all changes and ignore further changes.
+
+        IPage page = cycle.getPage();
+
+        IMarkupWriter writer = page.getResponseWriter(output);
+
+        output.setContentType(writer.getContentType());
+
+        boolean discard = true;
+
+        try
+        {
+            cycle.renderPage(writer);
+
+            discard = false;
+        }
+        finally
+        {
+            // Closing the writer closes its PrintWriter and a whole stack of java.io objects,
+            // which tend to stream a lot of output that eventually hits the
+            // ResponseOutputStream.  If we are discarding output anyway (due to an exception
+            // getting thrown during the render), we can save ourselves some trouble
+            // by ignoring it.
+
+            if (discard)
+                output.setDiscard(true);
+
+            writer.close();
+
+            if (discard)
+                output.setDiscard(false);
+        }
+
+    }
+
+    /**
+     * Invalidates the session, then redirects the client web browser to
+     * the servlet's prefix, starting a new visit.
+     *
+     * <p>Subclasses should perform their own restart (if necessary, which is
+     * rarely) before invoking this implementation.
+     *
+     **/
+
+    public void restart(IRequestCycle cycle) throws IOException
+    {
+        RequestContext context = cycle.getRequestContext();
+
+        HttpSession session = context.getSession();
+
+        if (session != null)
+        {
+            try
+            {
+                session.invalidate();
+            }
+            catch (IllegalStateException ex)
+            {
+                if (LOG.isDebugEnabled())
+                    LOG.debug("Exception thrown invalidating HttpSession.", ex);
+
+                // Otherwise, ignore it.
+            }
+        }
+
+        // Make isStateful() return false, so that the servlet doesn't
+        // try to store the engine back into the (now invalid) session.
+
+        _stateful = false;
+
+        String url = context.getAbsoluteURL(_servletPath);
+
+        context.redirect(url);
+    }
+
+    /**
+     *  Delegate method for the servlet.  Services the request.
+     *
+     **/
+
+    public boolean service(RequestContext context) throws ServletException, IOException
+    {
+        ApplicationServlet servlet = context.getServlet();
+        IRequestCycle cycle = null;
+        ResponseOutputStream output = null;
+        IMonitor monitor = null;
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Begin service " + context.getRequestURI());
+
+        if (_specification == null)
+            _specification = servlet.getApplicationSpecification();
+
+        // The servlet invokes setLocale() before invoking service().  We want
+        // to ignore that setLocale() ... that is, not force a cookie to be
+        // written.
+
+        _localeChanged = false;
+
+        if (_resolver == null)
+            _resolver = servlet.getResourceResolver();
+
+        try
+        {
+            setupForRequest(context);
+
+            monitor = getMonitor(context);
+
+            output = new ResponseOutputStream(context.getResponse());
+        }
+        catch (Exception ex)
+        {
+            reportException(Tapestry.getMessage("AbstractEngine.unable-to-begin-request"), ex);
+
+            throw new ServletException(ex.getMessage(), ex);
+        }
+
+        IEngineService service = null;
+
+        try
+        {
+            try
+            {
+                String serviceName;
+
+                try
+                {
+                    serviceName = extractServiceName(context);
+
+                    if (Tapestry.isBlank(serviceName))
+                        serviceName = Tapestry.HOME_SERVICE;
+
+                    // Must have a service to create the request cycle.
+                    // Must have a request cycle to report an exception.
+
+                    service = getService(serviceName);
+                }
+                catch (Exception ex)
+                {
+                    service = getService(Tapestry.HOME_SERVICE);
+                    cycle = createRequestCycle(context, service, monitor);
+
+                    throw ex;
+                }
+
+                cycle = createRequestCycle(context, service, monitor);
+
+                monitor.serviceBegin(serviceName, context.getRequestURI());
+
+                // Invoke the service, which returns true if it may have changed
+                // the state of the engine (most do return true).
+
+                service.service(this, cycle, output);
+
+                // Return true only if the engine is actually dirty.  This cuts down
+                // on the number of times the engine is stored into the
+                // session unceccesarily.
+
+                return _dirty;
+            }
+            catch (PageRedirectException ex)
+            {
+                handlePageRedirectException(ex, cycle, output);
+            }
+            catch (RedirectException ex)
+            {
+                handleRedirectException(cycle, ex);
+            }
+            catch (StaleLinkException ex)
+            {
+                handleStaleLinkException(ex, cycle, output);
+            }
+            catch (StaleSessionException ex)
+            {
+                handleStaleSessionException(ex, cycle, output);
+            }
+        }
+        catch (Exception ex)
+        {
+            monitor.serviceException(ex);
+
+            // Discard any output (if possible).  If output has already been sent to
+            // the client, then things get dicey.  Note that this block
+            // gets activated if the StaleLink or StaleSession pages throws
+            // any kind of exception.
+
+            // Attempt to switch to the exception page.  However, this may itself fail
+            // for a number of reasons, in which case a ServletException is thrown.
+
+            output.reset();
+
+            if (LOG.isDebugEnabled())
+                LOG.debug("Uncaught exception", ex);
+
+            activateExceptionPage(cycle, output, ex);
+        }
+        finally
+        {
+            if (service != null)
+                monitor.serviceEnd(service.getName());
+
+            try
+            {
+                cycle.cleanup();
+
+                // Closing the buffered output closes the underlying stream as well.
+
+                if (output != null)
+                    output.forceFlush();
+            }
+            catch (Exception ex)
+            {
+                reportException(Tapestry.getMessage("AbstractEngine.exception-during-cleanup"), ex);
+            }
+			finally
+			{
+                cleanupAfterRequest(cycle);
+			}
+
+            if (_disableCaching)
+            {
+                try
+                {
+                    clearCachedData();
+                }
+                catch (Exception ex)
+                {
+                    reportException(
+                        Tapestry.getMessage("AbstractEngine.exception-during-cache-clear"),
+                        ex);
+                }
+            }
+
+            if (LOG.isDebugEnabled())
+                LOG.debug("End service");
+
+        }
+
+        return _dirty;
+    }
+
+    /**
+     *  Handles {@link PageRedirectException} which involves
+     *  executing {@link IPage#validate(IRequestCycle)} on the target page
+     *  (of the exception), until either a loop is found, or a page
+     *  succesfully validates and can be activated.
+     *
+     *  <p>This should generally not be overriden in subclasses.
+     *
+     *  @since 3.0
+     */
+
+    protected void handlePageRedirectException(
+        PageRedirectException ex,
+        IRequestCycle cycle,
+        ResponseOutputStream output)
+        throws IOException, ServletException
+    {
+        List pageNames = new ArrayList();
+
+        String pageName = ex.getTargetPageName();
+
+        while (true)
+        {
+            if (pageNames.contains(pageName))
+            {
+                // Add the offending page to pageNames so it shows in the
+                // list.
+
+                pageNames.add(pageName);
+
+                StringBuffer buffer = new StringBuffer();
+                int count = pageNames.size();
+
+                for (int i = 0; i < count; i++)
+                {
+                    if (i > 0)
+                        buffer.append("; ");
+
+                    buffer.append(pageNames.get(i));
+                }
+
+                throw new ApplicationRuntimeException(
+                    Tapestry.format("AbstractEngine.validate-cycle", buffer.toString()));
+            }
+
+            // Record that this page has been a target.
+
+            pageNames.add(pageName);
+
+            try
+            {
+                // Attempt to activate the new page.
+
+                cycle.activate(pageName);
+
+                break;
+            }
+            catch (PageRedirectException ex2)
+            {
+                pageName = ex2.getTargetPageName();
+            }
+        }
+
+        // Discard any output from the previous page.
+
+        output.reset();
+
+        renderResponse(cycle, output);
+    }
+
+    /**
+     *  Invoked from {@link #service(RequestContext)} to create an instance of
+     *  {@link IRequestCycle} for the current request.  This implementation creates
+     *  an returns an instance of {@link RequestCycle}.
+     *
+     *  @since 3.0
+     *
+     **/
+
+    protected IRequestCycle createRequestCycle(
+        RequestContext context,
+        IEngineService service,
+        IMonitor monitor)
+    {
+        return new RequestCycle(this, context, service, monitor);
+    }
+
+    /**
+     *  Invoked by {@link #service(RequestContext)} if a {@link StaleLinkException}
+     *  is thrown by the {@link IEngineService service}.  This implementation
+     *  sets the message property of the StaleLink page to the
+     *  message provided in the exception,
+     *  then invokes
+     *  {@link #redirect(String, IRequestCycle, ResponseOutputStream, ApplicationRuntimeException)}
+     *  to render the StaleLink page.
+     *
+     *  <p>Subclasses may overide this method (without
+     *  invoking this implementation).  A common practice
+     *  is to present an error message on the application's
+     *  Home page.
+     *
+     *  <p>Alternately, the application may provide its own version of
+     *  the StaleLink page, overriding
+     *  the framework's implementation (probably a good idea, because the
+     *  default page hints at "application errors" and isn't localized).
+     *  The overriding StaleLink implementation must
+     *  implement a message property of type String.
+     *
+     *  @since 0.2.10
+     *
+     **/
+
+    protected void handleStaleLinkException(
+        StaleLinkException ex,
+        IRequestCycle cycle,
+        ResponseOutputStream output)
+        throws IOException, ServletException
+    {
+        String staleLinkPageName = getStaleLinkPageName();
+        IPage page = cycle.getPage(staleLinkPageName);
+
+        page.setProperty("message", ex.getMessage());
+
+        redirect(staleLinkPageName, cycle, output, ex);
+    }
+
+    /**
+     *  Invoked by {@link #service(RequestContext)} if a {@link StaleSessionException}
+     *  is thrown by the {@link IEngineService service}.  This implementation
+     *  invokes
+     *  {@link #redirect(String, IRequestCycle, ResponseOutputStream, ApplicationRuntimeException)}
+     *  to render the StaleSession page.
+     *
+     *  <p>Subclasses may overide this method (without
+     *  invoking this implementation).  A common practice
+     *  is to present an eror message on the application's
+     *  Home page.
+     *
+     *  @since 0.2.10
+     **/
+
+    protected void handleStaleSessionException(
+        StaleSessionException ex,
+        IRequestCycle cycle,
+        ResponseOutputStream output)
+        throws IOException, ServletException
+    {
+        redirect(getStaleSessionPageName(), cycle, output, ex);
+    }
+
+    /**
+     *  Discards all cached pages, component specifications and templates.
+     *  Subclasses who override this method should invoke this implementation
+     *  as well.
+     *
+     *  @since 1.0.1
+     *
+     **/
+
+    public void clearCachedData()
+    {
+        _pool.clear();
+        _pageSource.reset();
+        _specificationSource.reset();
+        _templateSource.reset();
+        _scriptSource.reset();
+        _stringsSource.reset();
+        _enhancer.reset();
+        _resourceChecksumSource.reset();
+    }
+
+    /**
+     *  Changes the locale for the engine.
+     *
+     **/
+
+    public void setLocale(Locale value)
+    {
+        if (value == null)
+            throw new IllegalArgumentException("May not change engine locale to null.");
+
+        // Because locale changes are expensive (it involves writing a cookie and all that),
+        // we're careful not to really change unless there's a true change in value.
+
+        if (!value.equals(_locale))
+        {
+            _locale = value;
+            _localeChanged = true;
+            markDirty();
+        }
+    }
+
+    /**
+     *  Invoked from {@link #service(RequestContext)} to ensure that the engine's
+     *  instance variables are setup.  This allows the application a chance to
+     *  restore transient variables that will not have survived deserialization.
+     *
+     *  Determines the servlet prefix:  this is the base URL used by
+     *  {@link IEngineService services} to build URLs.  It consists
+     *  of two parts:  the context path and the servlet path.
+     *
+     *  <p>The servlet path is retrieved from {@link HttpServletRequest#getServletPath()}.
+     *
+     *  <p>The context path is retrieved from {@link HttpServletRequest#getContextPath()}.
+     *
+     *  <p>The global object is retrieved from {@link IEngine#getGlobal()} method.
+     *
+     *  <p>The final path is available via the {@link #getServletPath()} method.
+     *
+     *  <p>In addition, this method locates and/or creates the:
+     *  <ul>
+     *  <li>{@link IComponentClassEnhancer}
+     *  <li>{@link Pool}
+     *  <li>{@link ITemplateSource}
+     *  <li>{@link ISpecificationSource}
+     *  <li>{@link IPageSource}
+     *  <li>{@link IEngineService} {@link Map}
+     *  <ll>{@link IScriptSource}
+     *  <li>{@link IComponentMessagesSource}
+     *  <li>{@link IPropertySource}
+     *  </ul>
+     *
+     *  <p>This order is important, because some of the later shared objects
+     *  depend on some of the earlier shared objects already having
+     *  been located or created
+     *  (especially {@link #getPool() pool}).
+     *
+     *  <p>Subclasses should invoke this implementation first, then perform their
+     *  own setup.
+     *
+     **/
+
+    protected void setupForRequest(RequestContext context)
+    {
+        HttpServlet servlet = context.getServlet();
+        ServletContext servletContext = servlet.getServletContext();
+        HttpServletRequest request = context.getRequest();
+        HttpSession session = context.getSession();
+
+        if (session != null)
+            _sessionId = context.getSession().getId();
+        else
+            _sessionId = null;
+
+        // Previously, this used getRemoteHost(), but that requires an
+        // expensive reverse DNS lookup. Possibly, the host name lookup
+        // should occur ... but only if there's an actual error message
+        // to display.
+
+        if (_clientAddress == null)
+            _clientAddress = request.getRemoteAddr();
+
+        // servletPath is null, so this means either we're doing the
+        // first request in this session, or we're handling a subsequent
+        // request in another JVM (i.e. another server in the cluster).
+        // In any case, we have to do some late (re-)initialization.
+
+        if (_servletPath == null)
+        {
+            // Get the path *within* the servlet context
+
+            // In rare cases related to the tagsupport service, getServletPath() is wrong
+            // (its a JSP, which invokes Tapestry as an include, thus muddling what
+            // the real servlet and servlet path is).  In those cases, the JSP tag
+            // will inform us.
+
+            String path =
+                (String) request.getAttribute(Tapestry.TAG_SUPPORT_SERVLET_PATH_ATTRIBUTE);
+
+            if (path == null)
+                path = request.getServletPath();
+
+            // Get the context path, which may be the empty string
+            // (but won't be null).
+
+            _contextPath = request.getContextPath();
+
+            _servletPath = _contextPath + path;
+        }
+
+        String servletName = context.getServlet().getServletName();
+
+        if (_propertySource == null)
+        {
+            String name = PROPERTY_SOURCE_NAME + ":" + servletName;
+
+            _propertySource = (IPropertySource) servletContext.getAttribute(name);
+
+            if (_propertySource == null)
+            {
+                _propertySource = createPropertySource(context);
+
+                servletContext.setAttribute(name, _propertySource);
+            }
+        }
+
+        if (_enhancer == null)
+        {
+            String name = ENHANCER_NAME + ":" + servletName;
+
+            _enhancer = (IComponentClassEnhancer) servletContext.getAttribute(name);
+
+            if (_enhancer == null)
+            {
+                _enhancer = createComponentClassEnhancer(context);
+
+                servletContext.setAttribute(name, _enhancer);
+            }
+        }
+
+        if (_pool == null)
+        {
+            String name = POOL_NAME + ":" + servletName;
+
+            _pool = (Pool) servletContext.getAttribute(name);
+
+            if (_pool == null)
+            {
+                _pool = createPool(context);
+
+                servletContext.setAttribute(name, _pool);
+            }
+        }
+
+        if (_templateSource == null)
+        {
+            String name = TEMPLATE_SOURCE_NAME + ":" + servletName;
+
+            _templateSource = (ITemplateSource) servletContext.getAttribute(name);
+
+            if (_templateSource == null)
+            {
+                _templateSource = createTemplateSource(context);
+
+                servletContext.setAttribute(name, _templateSource);
+            }
+        }
+
+        if (_specificationSource == null)
+        {
+            String name = SPECIFICATION_SOURCE_NAME + ":" + servletName;
+
+            _specificationSource = (ISpecificationSource) servletContext.getAttribute(name);
+
+            if (_specificationSource == null)
+            {
+                _specificationSource = createSpecificationSource(context);
+
+                servletContext.setAttribute(name, _specificationSource);
+            }
+        }
+
+        if (_pageSource == null)
+        {
+            String name = PAGE_SOURCE_NAME + ":" + servletName;
+
+            _pageSource = (IPageSource) servletContext.getAttribute(name);
+
+            if (_pageSource == null)
+            {
+                _pageSource = createPageSource(context);
+
+                servletContext.setAttribute(name, _pageSource);
+            }
+        }
+
+        if (_scriptSource == null)
+        {
+            String name = SCRIPT_SOURCE_NAME + ":" + servletName;
+
+            _scriptSource = (IScriptSource) servletContext.getAttribute(name);
+
+            if (_scriptSource == null)
+            {
+                _scriptSource = createScriptSource(context);
+
+                servletContext.setAttribute(name, _scriptSource);
+            }
+        }
+
+        if (_serviceMap == null)
+        {
+            String name = SERVICE_MAP_NAME + ":" + servletName;
+
+            _serviceMap = (Map) servletContext.getAttribute(name);
+
+            if (_serviceMap == null)
+            {
+                _serviceMap = createServiceMap();
+
+                servletContext.setAttribute(name, _serviceMap);
+            }
+        }
+
+        if (_stringsSource == null)
+        {
+            String name = STRINGS_SOURCE_NAME + ":" + servletName;
+
+            _stringsSource = (IComponentMessagesSource) servletContext.getAttribute(name);
+
+            if (_stringsSource == null)
+            {
+                _stringsSource = createComponentStringsSource(context);
+
+                servletContext.setAttribute(name, _stringsSource);
+            }
+        }
+
+        if (_dataSqueezer == null)
+        {
+            String name = DATA_SQUEEZER_NAME + ":" + servletName;
+
+            _dataSqueezer = (DataSqueezer) servletContext.getAttribute(name);
+
+            if (_dataSqueezer == null)
+            {
+                _dataSqueezer = createDataSqueezer();
+
+                servletContext.setAttribute(name, _dataSqueezer);
+            }
+        }
+
+        if (_global == null)
+        {
+            String name = GLOBAL_NAME + ":" + servletName;
+
+            _global = servletContext.getAttribute(name);
+
+            if (_global == null)
+            {
+                _global = createGlobal(context);
+
+                servletContext.setAttribute(name, _global);
+            }
+        }
+        
+        if (_resourceChecksumSource == null)
+        {
+            String name = RESOURCE_CHECKSUM_SOURCE_NAME + ":" + servletName;
+
+            _resourceChecksumSource = (ResourceChecksumSource) servletContext.getAttribute(name);
+
+            if (_resourceChecksumSource == null)
+            {
+                _resourceChecksumSource = createResourceChecksumSource();
+
+                servletContext.setAttribute(name, _resourceChecksumSource);
+            }
+        }
+
+        String encoding = request.getCharacterEncoding();
+        if (encoding == null)
+        {
+            encoding = getOutputEncoding();
+            try
+            {
+                request.setCharacterEncoding(encoding);
+            }
+            catch (UnsupportedEncodingException e)
+            {
+                throw new IllegalArgumentException(Tapestry.format("illegal-encoding", encoding));
+            }
+            catch (NoSuchMethodError e)
+            {
+                // Servlet API 2.2 compatibility
+                // Behave okay if the setCharacterEncoding() method is unavailable
+            }
+            catch (AbstractMethodError e)
+            {
+                // Servlet API 2.2 compatibility
+                // Behave okay if the setCharacterEncoding() method is unavailable
+            }
+        }
+    }
+
+    /**
+     *
+     *  Invoked from {@link #setupForRequest(RequestContext)} to provide
+     *  a new instance of {@link IComponentMessagesSource}.
+     *
+     *  @return an instance of {@link DefaultComponentMessagesSource}
+     *  @since 2.0.4
+     *
+     **/
+
+    public IComponentMessagesSource createComponentStringsSource(RequestContext context)
+    {
+        return new DefaultComponentMessagesSource();
+    }
+
+    /**
+     *  Invoked from {@link #setupForRequest(RequestContext)} to provide
+     *  an instance of {@link IScriptSource} that will be stored into
+     *  the {@link ServletContext}.  Subclasses may override this method
+     *  to provide a different implementation.
+     *
+     *
+     *  @return an instance of {@link DefaultScriptSource}
+     *  @since 1.0.9
+     *
+     **/
+
+    protected IScriptSource createScriptSource(RequestContext context)
+    {
+        return new DefaultScriptSource(getResourceResolver());
+    }
+
+    /**
+     *  Invoked from {@link #setupForRequest(RequestContext)} to provide
+     *  an instance of {@link IPageSource} that will be stored into
+     *  the {@link ServletContext}.  Subclasses may override this method
+     *  to provide a different implementation.
+     *
+     *  @return an instance of {@link PageSource}
+     *  @since 1.0.9
+     *
+     **/
+
+    protected IPageSource createPageSource(RequestContext context)
+    {
+        return new PageSource(this);
+    }
+
+    /**
+     *  Invoked from {@link #setupForRequest(RequestContext)} to provide
+     *  an instance of {@link ISpecificationSource} that will be stored into
+     *  the {@link ServletContext}.  Subclasses may override this method
+     *  to provide a different implementation.
+     *
+     *  @return an instance of {@link DefaultSpecificationSource}
+     *  @since 1.0.9
+     **/
+
+    protected ISpecificationSource createSpecificationSource(RequestContext context)
+    {
+        return new DefaultSpecificationSource(getResourceResolver(), _specification, _pool);
+    }
+
+    /**
+     *  Invoked from {@link #setupForRequest(RequestContext)} to provide
+     *  an instance of {@link ITemplateSource} that will be stored into
+     *  the {@link ServletContext}.  Subclasses may override this method
+     *  to provide a different implementation.
+     *
+     *  @return an instance of {@link DefaultTemplateSource}
+     *  @since 1.0.9
+     *
+     **/
+
+    protected ITemplateSource createTemplateSource(RequestContext context)
+    {
+        return new DefaultTemplateSource();
+    }
+
+    /**
+     * Invoked from {@link #setupForRequest(RequestContext)} to provide
+     * an instance of {@link ResourceChecksumSource} that will be stored into
+     * the {@link ServletContext}.  Subclasses may override this method
+     * to provide a different implementation.
+     * @return an instance of {@link ResourceChecksumSourceImpl} that uses MD5 and Hex encoding.
+     * @since 3.0.3
+     */
+    protected ResourceChecksumSource createResourceChecksumSource()
+    {
+        return new ResourceChecksumSourceImpl("MD5", new Hex());
+    }
+    
+    /**
+     *  Returns an object which can find resources and classes.
+     *
+     **/
+
+    public IResourceResolver getResourceResolver()
+    {
+        return _resolver;
+    }
+
+    /**
+     *  Generates a description of the instance.
+     *  Invokes {@link #extendDescription(ToStringBuilder)}
+     *  to fill in details about the instance.
+     *
+     *  @see #extendDescription(ToStringBuilder)
+     *
+     **/
+
+    public String toString()
+    {
+        ToStringBuilder builder = new ToStringBuilder(this);
+
+        builder.append(
+            "name",
+            _specification == null
+                ? Tapestry.getMessage("AbstractEngine.unknown-specification")
+                : _specification.getName());
+
+        builder.append("dirty", _dirty);
+        builder.append("locale", _locale);
+        builder.append("stateful", _stateful);
+        builder.append("visit", _visit);
+
+        extendDescription(builder);
+
+        return builder.toString();
+    }
+
+    /**
+     *  Returns true if the reset service is curently enabled.
+     *
+     **/
+
+    public boolean isResetServiceEnabled()
+    {
+        return _resetServiceEnabled;
+    }
+
+    /**
+     *  Implemented by subclasses to return the names of the active pages
+     *  (pages for which recorders exist).  May return the empty list,
+     *  but should not return null.
+     *
+     **/
+
+    abstract public Collection getActivePageNames();
+
+    /**
+     *  Gets the visit object, if it has been created already.
+     *
+     *  <p>
+     *  If the visit is non-null then
+     *  the {@link #isDirty()} flag is set (because
+     *  the engine can't tell what the caller will
+     *  <i>do</i> with the visit).
+     *
+     **/
+
+    public Object getVisit()
+    {
+        if (_visit != null)
+            markDirty();
+
+        return _visit;
+    }
+
+    /**
+     *  Gets the visit object, invoking {@link #createVisit(IRequestCycle)} to create
+     *  it lazily if needed.  If cycle is null, the visit will not be lazily created.
+     *
+     *  <p>
+     *  After creating the visit, but before returning,
+     *  the {@link HttpSession} will be created, and
+     *  {@link #setStateful()} will be invoked.
+     *
+     *  <p>
+     *  Sets the {@link #isDirty()} flag, if the return value
+     *  is not null.
+     *
+     *
+     **/
+
+    public Object getVisit(IRequestCycle cycle)
+    {
+        if (_visit == null && cycle != null)
+        {
+            _visit = createVisit(cycle);
+
+            // Now that a visit object exists, we need to force the creation
+            // of a HttpSession.
+
+            cycle.getRequestContext().createSession();
+
+            setStateful();
+        }
+
+        if (_visit != null)
+            markDirty();
+
+        return _visit;
+    }
+
+    /**
+     *  Updates the visit object and
+     *  sets the {@link #isDirty() dirty flag}.
+     *
+     **/
+
+    public void setVisit(Object value)
+    {
+        _visit = value;
+
+        markDirty();
+    }
+
+    public boolean getHasVisit()
+    {
+        return _visit != null;
+    }
+
+    /**
+     *  Invoked to lazily create a new visit object when it is first
+     *  referenced (by {@link #getVisit(IRequestCycle)}).  This implementation works
+     *  by looking up the name of the class to instantiate
+     *  in the {@link #getPropertySource() configuration}.
+     *
+     *  <p>Subclasses may want to overide this method if some other means
+     *  of instantiating a visit object is required.
+     **/
+
+    protected Object createVisit(IRequestCycle cycle)
+    {
+        String visitClassName;
+        Class visitClass;
+        Object result = null;
+
+        visitClassName = _propertySource.getPropertyValue(VISIT_CLASS_PROPERTY_NAME);
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Creating visit object as instance of " + visitClassName);
+
+        visitClass = _resolver.findClass(visitClassName);
+
+        try
+        {
+            result = visitClass.newInstance();
+        }
+        catch (Throwable t)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("AbstractEngine.unable-to-instantiate-visit", visitClassName),
+                t);
+        }
+
+        return result;
+    }
+
+    /**
+     *  Returns the global object for the application.  The global object is created at the start
+     *  of the request ({@link #setupForRequest(RequestContext)} invokes
+     *  {@link #createGlobal(RequestContext)} if needed),
+     *  and is stored into the {@link ServletContext}.  All instances of the engine for
+     *  the application share
+     *  the global object; however, the global object is explicitly <em>not</em>
+     *  replicated to other servers within
+     *  a cluster.
+     *
+     *  @since 2.3
+     *
+     **/
+
+    public Object getGlobal()
+    {
+        return _global;
+    }
+
+    public IScriptSource getScriptSource()
+    {
+        return _scriptSource;
+    }
+
+    public boolean isStateful()
+    {
+        return _stateful;
+    }
+
+    /**
+     *  Invoked by subclasses to indicate that some state must now be stored
+     *  in the engine (and that the engine should now be stored in the
+     *  HttpSession).  The caller is responsible for actually creating
+     *  the HttpSession (it will have access to the {@link RequestContext}).
+     *
+     *  @since 1.0.2
+     *
+     **/
+
+    protected void setStateful()
+    {
+        _stateful = true;
+    }
+
+    /**
+     *  Allows subclasses to include listener methods easily.
+     *
+     * @since 1.0.2
+     **/
+
+    public ListenerMap getListeners()
+    {
+        if (_listeners == null)
+            _listeners = new ListenerMap(this);
+
+        return _listeners;
+    }
+
+    private static class RedirectAnalyzer
+    {
+        private boolean _internal;
+        private String _location;
+
+        private RedirectAnalyzer(String location)
+        {
+            if (Tapestry.isBlank(location))
+            {
+                _location = "";
+                _internal = true;
+
+                return;
+            }
+
+            _location = location;
+
+            _internal = !(location.startsWith("/") || location.indexOf("://") > 0);
+        }
+
+        public void process(IRequestCycle cycle)
+        {
+            RequestContext context = cycle.getRequestContext();
+
+            if (_internal)
+                forward(context);
+            else
+                redirect(context);
+        }
+
+        private void forward(RequestContext context)
+        {
+            HttpServletRequest request = context.getRequest();
+            HttpServletResponse response = context.getResponse();
+
+            RequestDispatcher dispatcher = request.getRequestDispatcher("/" + _location);
+
+            if (dispatcher == null)
+                throw new ApplicationRuntimeException(
+                    Tapestry.format("AbstractEngine.unable-to-find-dispatcher", _location));
+
+            try
+            {
+                dispatcher.forward(request, response);
+            }
+            catch (ServletException ex)
+            {
+                throw new ApplicationRuntimeException(
+                    Tapestry.format("AbstractEngine.unable-to-forward", _location),
+                    ex);
+            }
+            catch (IOException ex)
+            {
+                throw new ApplicationRuntimeException(
+                    Tapestry.format("AbstractEngine.unable-to-forward", _location),
+                    ex);
+            }
+        }
+
+        private void redirect(RequestContext context)
+        {
+            HttpServletResponse response = context.getResponse();
+
+            String finalURL = response.encodeRedirectURL(_location);
+
+            try
+            {
+                response.sendRedirect(finalURL);
+            }
+            catch (IOException ex)
+            {
+                throw new ApplicationRuntimeException(
+                    Tapestry.format("AbstractEngine.unable-to-redirect", _location),
+                    ex);
+            }
+        }
+
+    }
+
+    /**
+     *  Invoked when a {@link RedirectException} is thrown during the processing of a request.
+     *
+     *  @throws ApplicationRuntimeException if an {@link IOException},
+     *  {@link ServletException} is thrown by the redirect, or if no
+     *  {@link RequestDispatcher} can be found for local resource.
+     *
+     *  @since 2.2
+     *
+     **/
+
+    protected void handleRedirectException(IRequestCycle cycle, RedirectException ex)
+    {
+        String location = ex.getRedirectLocation();
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Redirecting to: " + location);
+
+        RedirectAnalyzer analyzer = new RedirectAnalyzer(location);
+
+        analyzer.process(cycle);
+    }
+
+    /**
+     *  Creates a Map of all the services available to the application.
+     *
+     *  <p>Note: the Map returned is not synchronized, on the theory that returned
+     *  map is not further modified and therefore threadsafe.
+     *
+     **/
+
+    private Map createServiceMap()
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Creating service map.");
+
+        ISpecificationSource source = getSpecificationSource();
+
+        // Build the initial version of the result map,
+        // where each value is the *name* of a class.
+
+        Map result = new HashMap();
+
+        // Do the framework first.
+
+        addServices(source.getFrameworkNamespace(), result);
+
+        // And allow the application to override the framework.
+
+        addServices(source.getApplicationNamespace(), result);
+
+        IResourceResolver resolver = getResourceResolver();
+
+        Iterator i = result.entrySet().iterator();
+
+        while (i.hasNext())
+        {
+            Map.Entry entry = (Map.Entry) i.next();
+
+            String name = (String) entry.getKey();
+            String className = (String) entry.getValue();
+
+            if (LOG.isDebugEnabled())
+                LOG.debug("Creating service " + name + " as instance of " + className);
+
+            Class serviceClass = resolver.findClass(className);
+
+            try
+            {
+                IEngineService service = (IEngineService) serviceClass.newInstance();
+                String serviceName = service.getName();
+
+                if (!service.getName().equals(name))
+                    throw new ApplicationRuntimeException(
+                        Tapestry.format(
+                            "AbstractEngine.service-name-mismatch",
+                            name,
+                            className,
+                            serviceName));
+
+                // Replace the class name with an instance
+                // of the named class.
+
+                entry.setValue(service);
+            }
+            catch (InstantiationException ex)
+            {
+                String message =
+                    Tapestry.format(
+                        "AbstractEngine.unable-to-instantiate-service",
+                        name,
+                        className);
+
+                LOG.error(message, ex);
+
+                throw new ApplicationRuntimeException(message, ex);
+            }
+            catch (IllegalAccessException ex)
+            {
+                String message =
+                    Tapestry.format(
+                        "AbstractEngine.unable-to-instantiate-service",
+                        name,
+                        className);
+
+                LOG.error(message, ex);
+
+                throw new ApplicationRuntimeException(message, ex);
+            }
+        }
+
+        // Result should not be modified after this point, for threadsafety issues.
+        // We could wrap it in an unmodifiable, but for efficiency we don't.
+
+        return result;
+    }
+
+    /**
+     *  Locates all services in the namespace and adds key/value
+     *  pairs to the map (name and class name).  Then recursively
+     *  descendends into child namespaces to collect more
+     *  service names.
+     *
+     *  @since 2.2
+     *
+     **/
+
+    private void addServices(INamespace namespace, Map map)
+    {
+        List names = namespace.getServiceNames();
+        int count = names.size();
+
+        for (int i = 0; i < count; i++)
+        {
+            String name = (String) names.get(i);
+
+            map.put(name, namespace.getServiceClassName(name));
+        }
+
+        List namespaceIds = namespace.getChildIds();
+        count = namespaceIds.size();
+
+        for (int i = 0; i < count; i++)
+        {
+            String id = (String) namespaceIds.get(i);
+
+            addServices(namespace.getChildNamespace(id), map);
+        }
+    }
+
+    /**
+     *  @since 2.0.4
+     *
+     **/
+
+    public IComponentMessagesSource getComponentMessagesSource()
+    {
+        return _stringsSource;
+    }
+
+    /**
+     *  @since 2.2
+     *
+     **/
+
+    public DataSqueezer getDataSqueezer()
+    {
+        return _dataSqueezer;
+    }
+
+    /**
+     *
+     *  Invoked from {@link #setupForRequest(RequestContext)} to create
+     *  a {@link DataSqueezer} when needed (typically, just the very first time).
+     *  This implementation returns a standard, new instance.
+     *
+     *  @since 2.2
+     *
+     **/
+
+    public DataSqueezer createDataSqueezer()
+    {
+        return new DataSqueezer(_resolver);
+    }
+
+    /**
+     *  Invoked from {@link #service(RequestContext)} to extract, from the URL,
+     *  the name of the service.  The current implementation expects the first
+     *  pathInfo element to be the service name.  At some point in the future,
+     *  the method of constructing and parsing URLs may be abstracted into
+     *  a developer-selected class.
+     *
+     *  <p>Subclasses may override this method if the application defines
+     *  specific services with unusual URL encoding rules.
+     *
+     *  <p>This implementation simply extracts the value for
+     *  query parameter {@link Tapestry#SERVICE_QUERY_PARAMETER_NAME}
+     *  and extracts the service name from that.
+     *
+     *  <p>
+     *  For supporting the JSP tags, this method first
+     *  checks for attribute {@link Tapestry#TAG_SUPPORT_SERVICE_ATTRIBUTE}.  If non-null,
+     *  then {@link Tapestry#TAGSUPPORT_SERVICE} is returned.
+     *
+     *  @since 2.2
+     *
+     **/
+
+    protected String extractServiceName(RequestContext context)
+    {
+        if (context.getRequest().getAttribute(Tapestry.TAG_SUPPORT_SERVICE_ATTRIBUTE) != null)
+            return Tapestry.TAGSUPPORT_SERVICE;
+
+        String serviceData = context.getParameter(Tapestry.SERVICE_QUERY_PARAMETER_NAME);
+
+        if (serviceData == null)
+            return Tapestry.HOME_SERVICE;
+
+        // The service name is anything before the first slash,
+        // if there is one.
+
+        int slashx = serviceData.indexOf('/');
+
+        if (slashx < 0)
+            return serviceData;
+
+        return serviceData.substring(0, slashx);
+    }
+
+    /** @since 2.3 **/
+
+    public IPropertySource getPropertySource()
+    {
+        return _propertySource;
+    }
+
+    /** @since 3.0.3 */
+    
+    public ResourceChecksumSource getResourceChecksumSource()
+    {
+        return _resourceChecksumSource;
+    }
+    
+    /** @since 3.0 **/
+
+    protected String getExceptionPageName()
+    {
+        return EXCEPTION_PAGE;
+    }
+
+    /** @since 3.0 **/
+
+    protected String getStaleLinkPageName()
+    {
+        return STALE_LINK_PAGE;
+    }
+
+    /** @since 3.0 **/
+
+    protected String getStaleSessionPageName()
+    {
+        return STALE_SESSION_PAGE;
+    }
+
+    /**
+     *  Name of an application extension that can provide configuration properties.
+     *
+     *  @see #createPropertySource(RequestContext)
+     *  @since 2.3
+     *
+     **/
+
+    private static final String EXTENSION_PROPERTY_SOURCE_NAME =
+        "org.apache.tapestry.property-source";
+
+    /**
+     *  Creates a shared property source that will be stored into
+     *  the servlet context.
+     *  Subclasses may override this method to build thier
+     *  own search path.
+     *
+     *  <p>If the application specification contains an extension
+     *  named "org.apache.tapestry.property-source" it is inserted
+     *  in the search path just before
+     *  the property source for JVM System Properties.  This is a simple
+     *  hook at allow application-specific methods of obtaining
+     *  configuration values (typically, from a database or from JMX,
+     *  in some way).  Alternately, subclasses may
+     *  override this method to provide whatever search path
+     *  is appropriate.
+     *
+     *
+     *  @since 2.3
+     *
+     **/
+
+    protected IPropertySource createPropertySource(RequestContext context)
+    {
+        DelegatingPropertySource result = new DelegatingPropertySource();
+
+        ApplicationServlet servlet = context.getServlet();
+        IApplicationSpecification spec = servlet.getApplicationSpecification();
+
+        result.addSource(new PropertyHolderPropertySource(spec));
+        result.addSource(new ServletPropertySource(servlet.getServletConfig()));
+        result.addSource(new ServletContextPropertySource(servlet.getServletContext()));
+
+        if (spec.checkExtension(EXTENSION_PROPERTY_SOURCE_NAME))
+        {
+            IPropertySource source =
+                (IPropertySource) spec.getExtension(
+                    EXTENSION_PROPERTY_SOURCE_NAME,
+                    IPropertySource.class);
+
+            result.addSource(source);
+        }
+
+        result.addSource(SystemPropertiesPropertySource.getInstance());
+
+        // Lastly, add a final source to handle "factory defaults".
+
+        ResourceBundle bundle =
+            ResourceBundle.getBundle("org.apache.tapestry.ConfigurationDefaults");
+
+        result.addSource(new ResourceBundlePropertySource(bundle));
+
+        return result;
+    }
+
+    /**
+     *  Creates the shared Global object.  This implementation looks for an configuration
+     *  property, <code>org.apache.tapestry.global-class</code>, and instantiates that class
+     *  using a no-arguments
+     *  constructor.  If the property is not defined, a synchronized
+     *  {@link java.util.HashMap} is created.
+     *
+     *  @since 2.3
+     *
+     **/
+
+    protected Object createGlobal(RequestContext context)
+    {
+        String className = _propertySource.getPropertyValue("org.apache.tapestry.global-class");
+
+        if (className == null)
+            return Collections.synchronizedMap(new HashMap());
+
+        Class globalClass = _resolver.findClass(className);
+
+        try
+        {
+            return globalClass.newInstance();
+        }
+        catch (Exception ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("AbstractEngine.unable-to-instantiate-global", className),
+                ex);
+        }
+    }
+
+    /**
+     *  Returns an new instance of {@link Pool}, with the standard
+     *  set of adaptors, plus {@link BSFManagerPoolableAdaptor} for
+     *  {@link BSFManager}.
+     *
+     *  <p>Subclasses may override this
+     *  method to configure the Pool differently.
+     *
+     *  @since 3.0
+     *
+     **/
+
+    protected Pool createPool(RequestContext context)
+    {
+        Pool result = new Pool();
+
+        result.registerAdaptor(BSFManager.class, new BSFManagerPoolableAdaptor());
+
+        return result;
+    }
+
+    /** @since 3.0 **/
+
+    public Pool getPool()
+    {
+        return _pool;
+    }
+
+    /**
+     *
+     * Invoked from {@link #setupForRequest(RequestContext)}.  Creates
+     * a new instance of {@link DefaultComponentClassEnhancer}.  Subclasses
+     * may override to return a different object.
+     * 
+     * <p>
+     * Check the property <code>org.apache.tapestry.enhance.disable-abstract-method-validation</code>
+     * and, if true, disables abstract method validation. This is used  in some
+     * errant JDK's (such as IBM's 1.3.1) that incorrectly report concrete methods from
+     * abstract classes as abstract.
+     *
+     * @since 3.0
+     */
+
+    protected IComponentClassEnhancer createComponentClassEnhancer(RequestContext context)
+    {
+        boolean disableValidation =
+            "true".equals(
+                _propertySource.getPropertyValue(
+                    "org.apache.tapestry.enhance.disable-abstract-method-validation"));
+
+        return new DefaultComponentClassEnhancer(_resolver, disableValidation);
+    }
+
+    /** @since 3.0 **/
+
+    public IComponentClassEnhancer getComponentClassEnhancer()
+    {
+        return _enhancer;
+    }
+
+    /**
+     *  Returns true if the engine has (potentially) changed
+     *  state since the last time it was stored
+     *  into the {@link javax.servlet.http.HttpSession}.  Various
+     *  events set this property to true.
+     *
+     *  @since 3.0
+     *
+     **/
+
+    public boolean isDirty()
+    {
+        return _dirty;
+    }
+
+    /**
+     *  Invoked to set the dirty flag, indicating that the
+     *  engine should be stored into the
+     *  {@link javax.servlet.http.HttpSession}.
+     *
+     *
+     *  @since 3.0
+     *
+     **/
+
+    protected void markDirty()
+    {
+        if (!_dirty)
+            LOG.debug("Setting dirty flag.");
+
+        _dirty = true;
+    }
+
+    /**
+     *
+     *  Clears the dirty flag when a engine is stored into the
+     *  {@link HttpSession}.
+     *
+     *
+     *  @since 3.0
+     *
+     **/
+
+    public void valueBound(HttpSessionBindingEvent arg0)
+    {
+        LOG.debug(_dirty ? "Clearing dirty flag." : "Dirty flag already cleared.");
+
+        _dirty = false;
+    }
+
+    /**
+     *  Does nothing.
+     *
+     *  @since 3.0
+     *
+     **/
+
+    public void valueUnbound(HttpSessionBindingEvent arg0)
+    {
+    }
+
+    /**
+     *
+     *  The encoding to be used if none has been defined using the output encoding property.
+     *  Override this method to change the default.
+     *
+     *  @return the default output encoding
+     *  @since 3.0
+     *
+     **/
+    protected String getDefaultOutputEncoding()
+    {
+        return DEFAULT_OUTPUT_ENCODING;
+    }
+
+    /**
+     *
+     *  Returns the encoding to be used to generate the servlet responses and
+     *  accept the servlet requests.
+     *
+     *  The encoding is defined using the org.apache.tapestry.output-encoding
+     *  and is UTF-8 by default
+     *
+     *  @since 3.0
+     *  @see org.apache.tapestry.IEngine#getOutputEncoding()
+     *
+     **/
+    public String getOutputEncoding()
+    {
+        IPropertySource source = getPropertySource();
+
+        String encoding = source.getPropertyValue(OUTPUT_ENCODING_PROPERTY_NAME);
+        if (encoding == null)
+            encoding = getDefaultOutputEncoding();
+
+        return encoding;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/AbstractService.java b/tapestry-framework/src/org/apache/tapestry/engine/AbstractService.java
new file mode 100644
index 0000000..8cfd3d2
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/AbstractService.java
@@ -0,0 +1,127 @@
+//  Copyright 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.tapestry.engine;
+
+import java.io.IOException;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.request.RequestContext;
+import org.apache.tapestry.util.StringSplitter;
+import org.apache.tapestry.util.io.DataSqueezer;
+
+/**
+ *  Abstract base class for implementing engine services.  Instances of services
+ *  are shared by many engines and threads, so they must be threadsafe.
+ * 
+ *  <p>
+ *  Note; too much of the URL encoding/decoding stategy is fixed here.
+ *  A future release of Tapestry may extract this strategy, allowing developers
+ *  to choose the method via which URLs are encoded.
+ * 
+ *  @see org.apache.tapestry.engine.AbstractEngine#getService(String)
+ * 
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.3
+ * 
+ **/
+
+public abstract class AbstractService implements IEngineService
+{
+    /**
+     *  Constructs a link for the service.
+     *
+     *  @param cycle the request cycle
+     *  @param serviceName the name of the service
+     *  @param serviceContext context related to the service itself which is added to the URL as-is
+     *  @param parameters additional service parameters provided by the component; 
+     *  this is application specific information, and is encoded with 
+     *  {@link java.net.URLEncoder#encode(String)} before being added
+     *  to the query.
+     *  @param stateful if true, the final URL must be encoded with the HttpSession id
+     *
+     **/
+
+    protected ILink constructLink(
+        IRequestCycle cycle,
+        String serviceName,
+        String[] serviceContext,
+        Object[] parameters,
+        boolean stateful)
+    {
+        DataSqueezer squeezer = cycle.getEngine().getDataSqueezer();
+        String[] squeezed = null;
+
+        try
+        {
+            squeezed = squeezer.squeeze(parameters);
+        }
+        catch (IOException ex)
+        {
+            throw new ApplicationRuntimeException(ex);
+        }
+
+        return new EngineServiceLink(cycle, serviceName, serviceContext, squeezed, stateful);
+    }
+
+    /**
+     *  Returns the service context as an array of Strings.
+     *  Returns null if there are no service context strings.
+     *
+     **/
+
+    protected String[] getServiceContext(RequestContext context)
+    {
+        String service = context.getParameter(Tapestry.SERVICE_QUERY_PARAMETER_NAME);
+
+		int slashx = service.indexOf('/');
+		
+		if (slashx < 0)
+			return null;
+			
+		String serviceContext = service.substring(slashx + 1);
+
+        return new StringSplitter('/').splitToArray(serviceContext);
+    }
+
+    /**
+     *  Returns the service parameters as an array of Strings.
+     *
+     **/
+
+    protected Object[] getParameters(IRequestCycle cycle)
+    {
+        RequestContext context = cycle.getRequestContext();
+
+        String[] squeezed = context.getParameters(Tapestry.PARAMETERS_QUERY_PARAMETER_NAME);
+
+        if (Tapestry.size(squeezed) == 0)
+            return squeezed;
+
+        try
+        {
+            DataSqueezer squeezer = cycle.getEngine().getDataSqueezer();
+
+            return squeezer.unsqueeze(squeezed);
+        }
+        catch (IOException ex)
+        {
+            throw new ApplicationRuntimeException(ex);
+        }
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/ActionService.java b/tapestry-framework/src/org/apache/tapestry/engine/ActionService.java
new file mode 100644
index 0000000..ae48d8f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/ActionService.java
@@ -0,0 +1,173 @@
+//  Copyright 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.tapestry.engine;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpSession;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IAction;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IPage;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.StaleSessionException;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.request.ResponseOutputStream;
+
+/**
+ *  A context-sensitive service related to {@link org.apache.tapestry.form.Form} 
+ *  and {@link org.apache.tapestry.link.ActionLink}.  Encodes
+ *  the page, component and an action id in the service context.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.9
+ *
+ **/
+
+public class ActionService extends AbstractService
+{
+    /**
+     *  Encoded into URL if engine was stateful.
+     * 
+     *  @since 3.0
+     **/
+
+    private static final String STATEFUL_ON = "1";
+
+    /**
+     *  Encoded into URL if engine was not stateful.
+     * 
+     *  @since 3.0
+     **/
+
+    private static final String STATEFUL_OFF = "0";
+
+    public ILink getLink(IRequestCycle cycle, IComponent component, Object[] parameters)
+    {
+        if (parameters == null || parameters.length != 1)
+            throw new IllegalArgumentException(
+                Tapestry.format("service-single-parameter", Tapestry.ACTION_SERVICE));
+
+        String stateful = cycle.getEngine().isStateful() ? STATEFUL_ON : STATEFUL_OFF;
+        IPage componentPage = component.getPage();
+        IPage responsePage = cycle.getPage();
+
+        boolean complex = (componentPage != responsePage);
+
+        String[] serviceContext = new String[complex ? 5 : 4];
+
+        int i = 0;
+
+        serviceContext[i++] = stateful;
+        serviceContext[i++] = responsePage.getPageName();
+        serviceContext[i++] = (String) parameters[0];
+
+        // Because of Block/InsertBlock, the component may not be on
+        // the same page as the response page and we need to make
+        // allowances for this.
+
+        if (complex)
+            serviceContext[i++] = componentPage.getPageName();
+
+        serviceContext[i++] = component.getIdPath();
+
+        return constructLink(cycle, Tapestry.ACTION_SERVICE, serviceContext, null, true);
+    }
+
+    public void service(
+        IEngineServiceView engine,
+        IRequestCycle cycle,
+        ResponseOutputStream output)
+        throws ServletException, IOException
+    {
+        IAction action = null;
+        String componentPageName;
+        int count = 0;
+
+        String[] serviceContext = getServiceContext(cycle.getRequestContext());
+
+        if (serviceContext != null)
+            count = serviceContext.length;
+
+        if (count != 4 && count != 5)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("ActionService.context-parameters"));
+
+        boolean complex = count == 5;
+
+        int i = 0;
+        String stateful = serviceContext[i++];
+        String pageName = serviceContext[i++];
+        String targetActionId = serviceContext[i++];
+
+        if (complex)
+            componentPageName = serviceContext[i++];
+        else
+            componentPageName = pageName;
+
+        String targetIdPath = serviceContext[i++];
+
+        IPage page = cycle.getPage(pageName);
+
+		// Setup the page for the rewind, then do the rewind.
+
+		cycle.activate(page);
+		
+        IPage componentPage = cycle.getPage(componentPageName);
+        IComponent component = componentPage.getNestedComponent(targetIdPath);
+
+        try
+        {
+            action = (IAction) component;
+        }
+        catch (ClassCastException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("ActionService.component-wrong-type", component.getExtendedId()),
+                component,
+                null,
+                ex);
+        }
+
+        // Only perform the stateful check if the application was stateful
+        // when the URL was rendered.
+
+        if (stateful.equals(STATEFUL_ON) && action.getRequiresSession())
+        {
+            HttpSession session = cycle.getRequestContext().getSession();
+
+            if (session == null || session.isNew())
+                throw new StaleSessionException();
+        }
+
+        cycle.rewindPage(targetActionId, action);
+
+        // During the rewind, a component may change the page.  This will take
+        // effect during the second render, which renders the HTML response.
+
+        // Render the response.
+
+        engine.renderResponse(cycle, output);
+    }
+
+    public String getName()
+    {
+        return Tapestry.ACTION_SERVICE;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/BSFManagerPoolableAdaptor.java b/tapestry-framework/src/org/apache/tapestry/engine/BSFManagerPoolableAdaptor.java
new file mode 100644
index 0000000..fc20c7e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/BSFManagerPoolableAdaptor.java
@@ -0,0 +1,54 @@
+//  Copyright 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.tapestry.engine;
+
+import org.apache.bsf.BSFManager;
+import org.apache.tapestry.util.pool.IPoolableAdaptor;
+
+/**
+ *  Allows a {@link org.apache.tapestry.util.pool.Pool} to
+ *  properly terminate a {@link org.apache.bsf.BSFManager}
+ *  when it is discarded.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class BSFManagerPoolableAdaptor implements IPoolableAdaptor
+{
+    /**
+     *  Does nothing.
+     * 
+     **/
+    
+    public void resetForPool(Object object)
+    {
+    }
+
+    /**
+     *  Invokes {@link org.apache.bsf.BSFManager#terminate()}.
+     * 
+     **/
+    
+    public void discardFromPool(Object object)
+    {
+        BSFManager manager = (BSFManager)object;
+        
+        manager.terminate();
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/BaseEngine.java b/tapestry-framework/src/org/apache/tapestry/engine/BaseEngine.java
new file mode 100644
index 0000000..46f95c2
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/BaseEngine.java
@@ -0,0 +1,239 @@
+//  Copyright 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.tapestry.engine;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.record.SessionPageRecorder;
+
+/**
+ *  Concrete implementation of {@link org.apache.tapestry.IEngine} used for ordinary
+ *  applications.  All page state information is maintained in
+ *  the {@link javax.servlet.http.HttpSession} using
+ *  instances of {@link org.apache.tapestry.record.SessionPageRecorder}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class BaseEngine extends AbstractEngine
+{
+    private static final long serialVersionUID = -7051050643746333380L;
+
+    private final static int MAP_SIZE = 3;
+
+    private transient Map _recorders;
+
+    private transient Set _activePageNames;
+
+    /**
+     *  Removes all page recorders that contain no changes, or
+     *  are marked for discard.  Subclasses
+     *  should invoke this implementation in addition to providing
+     *  thier own.
+     *
+     **/
+
+    protected void cleanupAfterRequest(IRequestCycle cycle)
+    {
+        if (Tapestry.isEmpty(_recorders))
+            return;
+
+		boolean markDirty = false;
+        Iterator i = _recorders.entrySet().iterator();
+
+        while (i.hasNext())
+        {
+            Map.Entry entry = (Map.Entry) i.next();
+            String pageName = (String) entry.getKey();
+            IPageRecorder recorder = (IPageRecorder) entry.getValue();
+
+            if (!recorder.getHasChanges() || recorder.isMarkedForDiscard())
+            {
+                recorder.discard();
+
+                i.remove();
+
+                _activePageNames.remove(pageName);
+      	
+      			markDirty = true;
+            }
+        }
+        
+        if (markDirty)
+        	markDirty();
+    }
+
+    public void forgetPage(String name)
+    {
+        if (_recorders == null)
+            return;
+
+        IPageRecorder recorder = (IPageRecorder) _recorders.get(name);
+        if (recorder == null)
+            return;
+
+        if (recorder.isDirty())
+            throw new ApplicationRuntimeException(
+                Tapestry.format("BaseEngine.recorder-has-uncommited-changes", name));
+
+        recorder.discard();
+        _recorders.remove(name);
+        _activePageNames.remove(name);
+        
+        markDirty();
+    }
+
+    /**
+     *  Returns an unmodifiable {@link Collection} of the page names for which
+     *  {@link IPageRecorder} instances exist.
+     * 
+     *
+     **/
+
+    public Collection getActivePageNames()
+    {
+        if (_activePageNames == null)
+            return Collections.EMPTY_LIST;
+
+        return Collections.unmodifiableCollection(_activePageNames);
+    }
+
+    public IPageRecorder getPageRecorder(String pageName, IRequestCycle cycle)
+    {
+        if (_activePageNames == null || !_activePageNames.contains(pageName))
+            return null;
+
+        IPageRecorder result = null;
+
+        if (_recorders != null)
+            return result = (IPageRecorder) _recorders.get(pageName);
+
+        // So the page is active, but not in the cache of page recoders,
+        // so (re-)create the page recorder.
+
+        if (result == null)
+            result = createPageRecorder(pageName, cycle);
+
+        return result;
+    }
+
+    public IPageRecorder createPageRecorder(String pageName, IRequestCycle cycle)
+    {
+        if (_recorders == null)
+            _recorders = new HashMap(MAP_SIZE);
+        else
+        {
+            if (_recorders.containsKey(pageName))
+                throw new ApplicationRuntimeException(
+                    Tapestry.format("BaseEngine.duplicate-page-recorder", pageName));
+        }
+
+        // Force the creation of the HttpSession
+
+        cycle.getRequestContext().createSession();
+        setStateful();
+       
+
+        IPageRecorder result = new SessionPageRecorder();
+        result.initialize(pageName, cycle);
+
+        _recorders.put(pageName, result);
+
+        if (_activePageNames == null)
+            _activePageNames = new HashSet();
+
+        _activePageNames.add(pageName);
+        
+        markDirty();
+
+        return result;
+    }
+
+    /**
+     *  Reconstructs the list of active page names
+     *  written by {@link #writeExternal(ObjectOutput)}.
+     * 
+     **/
+
+    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
+    {
+        super.readExternal(in);
+
+        int count = in.readInt();
+
+        if (count > 0)
+            _activePageNames = new HashSet(count);
+
+        for (int i = 0; i < count; i++)
+        {
+            String name = in.readUTF();
+
+            _activePageNames.add(name);
+        }
+
+    }
+
+    /**
+     *  Writes the engine's persistent state; this is simply the list of active page
+     *  names.  For efficiency, this is written as a count followed by each name
+     *  as a UTF String.
+     * 
+     **/
+
+    public void writeExternal(ObjectOutput out) throws IOException
+    {
+        super.writeExternal(out);
+
+        if (Tapestry.isEmpty(_activePageNames))
+        {
+            out.writeInt(0);
+            return;
+        }
+
+        int count = _activePageNames.size();
+
+        out.writeInt(count);
+
+        Iterator i = _activePageNames.iterator();
+
+        while (i.hasNext())
+        {
+            String name = (String) i.next();
+
+            out.writeUTF(name);
+        }
+    }
+
+    public void extendDescription(ToStringBuilder builder)
+    {
+		builder.append("activePageNames", _activePageNames);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/ComponentMessages.java b/tapestry-framework/src/org/apache/tapestry/engine/ComponentMessages.java
new file mode 100644
index 0000000..e081309
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/ComponentMessages.java
@@ -0,0 +1,109 @@
+//  Copyright 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.tapestry.engine;
+
+import java.text.MessageFormat;
+import java.util.Locale;
+import java.util.Properties;
+
+import org.apache.tapestry.IMessages;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Implementation of {@link org.apache.tapestry.IMessages}.  This is basically
+ *  a wrapper around an instance of {@link Properties}.  This ensures
+ *  that the properties are, in fact, read-only (which ensures that
+ *  they don't have to be synchronized).
+ *
+ *  @author Howard Lewis Ship
+ *  @since 2.0.4
+ *
+ **/
+
+public class ComponentMessages implements IMessages
+{
+    private Properties _properties;
+    private Locale _locale;
+
+    public ComponentMessages(Locale locale, Properties properties)
+    {
+        _locale = locale;
+        _properties = properties;
+    }
+
+    public String getMessage(String key, String defaultValue)
+    {
+        return _properties.getProperty(key, defaultValue);
+    }
+
+    public String getMessage(String key)
+    {
+        String result = _properties.getProperty(key);
+
+        if (result == null)
+            result = "[" + key.toUpperCase() + "]";
+
+        return result;
+    }
+
+    public String format(String key, Object argument1, Object argument2, Object argument3)
+    {
+        return format(key, new Object[] { argument1, argument2, argument3 });
+    }
+
+    public String format(String key, Object argument1, Object argument2)
+    {
+        return format(key, new Object[] { argument1, argument2 });
+    }
+
+    public String format(String key, Object argument)
+    {
+        return format(key, new Object[] { argument });
+    }
+
+    public String format(String key, Object[] arguments)
+    {
+        String pattern = getMessage(key);
+
+        // This ugliness is mandated for JDK 1.3 compatibility, which has a bug 
+        // in MessageFormat ... the
+        // pattern is applied in the constructor, using the system default Locale,
+        // regardless of what locale is later specified!
+        // It appears that the problem does not exist in JDK 1.4.
+
+        MessageFormat messageFormat = new MessageFormat("");
+        messageFormat.setLocale(_locale);
+        messageFormat.applyPattern(pattern);
+
+        int count = Tapestry.size(arguments);
+
+        for (int i = 0; i < count; i++)
+        {
+            if (arguments[i] instanceof Throwable)
+            {
+                Throwable t = (Throwable) arguments[i];
+                String message = t.getMessage();
+
+                if (Tapestry.isNonBlank(message))
+                    arguments[i] = message;
+                else
+                    arguments[i] = t.getClass().getName();
+            }
+        }
+
+        return messageFormat.format(arguments);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/DefaultComponentMessagesSource.java b/tapestry-framework/src/org/apache/tapestry/engine/DefaultComponentMessagesSource.java
new file mode 100644
index 0000000..253c438
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/DefaultComponentMessagesSource.java
@@ -0,0 +1,245 @@
+//  Copyright 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.tapestry.engine;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IMessages;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.util.MultiKey;
+
+/**
+ *  Global object (stored in the servlet context) that accesses
+ *  localized properties for a component.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.0.4
+ *
+ **/
+
+public class DefaultComponentMessagesSource implements IComponentMessagesSource
+{
+    private static final Log LOG = LogFactory.getLog(DefaultComponentMessagesSource.class);
+
+    private Properties _emptyProperties = new Properties();
+
+    /**
+     *  Map of {@link Properties}, keyed on a {@link MultiKey} of
+     *  component specification path and locale.
+     * 
+     **/
+
+    private Map _cache = new HashMap();
+
+    /**
+     *  Returns an instance of {@link Properties} containing
+     *  the properly localized messages for the component,
+     *  in the {@link Locale} identified by the component's
+     *  containing page.
+     * 
+     **/
+
+    protected synchronized Properties getLocalizedProperties(IComponent component)
+    {
+        if (component == null)
+            throw new IllegalArgumentException(
+                Tapestry.format("invalid-null-parameter", "component"));
+
+        IResourceLocation specificationLocation =
+            component.getSpecification().getSpecificationLocation();
+        Locale locale = component.getPage().getLocale();
+
+        // Check to see if already in the cache
+
+        MultiKey key = buildKey(specificationLocation, locale);
+
+        Properties result = (Properties) _cache.get(key);
+
+        if (result != null)
+            return result;
+
+        // Not found, create it now.
+
+        result = assembleProperties(specificationLocation, locale);
+
+        _cache.put(key, result);
+
+        return result;
+    }
+
+    private static final String SUFFIX = ".properties";
+
+    private Properties assembleProperties(IResourceLocation baseResourceLocation, Locale locale)
+    {
+        boolean debug = LOG.isDebugEnabled();
+        if (debug)
+            LOG.debug("Assembling properties for " + baseResourceLocation + " " + locale);
+
+        String name = baseResourceLocation.getName();
+
+        int dotx = name.indexOf('.');
+        String baseName = name.substring(0, dotx);
+
+        String language = locale.getLanguage();
+        String country = locale.getCountry();
+        String variant = locale.getVariant();
+
+        Properties parent = (Properties) _cache.get(baseResourceLocation);
+
+        if (parent == null)
+        {
+            parent = readProperties(baseResourceLocation, baseName, null, null);
+
+            if (parent == null)
+                parent = _emptyProperties;
+
+            _cache.put(baseResourceLocation, parent);
+        }
+
+        Properties result = parent;
+
+        if (!Tapestry.isBlank(language))
+        {
+            Locale l = new Locale(language, "");
+            MultiKey key = buildKey(baseResourceLocation, l);
+
+            result = (Properties) _cache.get(key);
+
+            if (result == null)
+                result = readProperties(baseResourceLocation, baseName, l, parent);
+
+            _cache.put(key, result);
+
+            parent = result;
+        }
+        else
+            language = "";
+
+        if (Tapestry.isNonBlank(country))
+        {
+            Locale l = new Locale(language, country);
+            MultiKey key = buildKey(baseResourceLocation, l);
+
+            result = (Properties) _cache.get(key);
+
+            if (result == null)
+                result = readProperties(baseResourceLocation, baseName, l, parent);
+
+            _cache.put(key, result);
+
+            parent = result;
+        }
+        else
+            country = "";
+
+        if (Tapestry.isNonBlank(variant))
+        {
+            Locale l = new Locale(language, country, variant);
+            MultiKey key = buildKey(baseResourceLocation, l);
+
+            result = (Properties) _cache.get(key);
+
+            if (result == null)
+                result = readProperties(baseResourceLocation, baseName, l, parent);
+
+            _cache.put(key, result);
+        }
+
+        return result;
+    }
+
+    private MultiKey buildKey(IResourceLocation location, Locale locale)
+    {
+        return new MultiKey(new Object[] { location, locale.toString()}, false);
+    }
+
+    private Properties readProperties(
+        IResourceLocation baseLocation,
+        String baseName,
+        Locale locale,
+        Properties parent)
+    {
+        StringBuffer buffer = new StringBuffer(baseName);
+
+        if (locale != null)
+        {
+            buffer.append('_');
+            buffer.append(locale.toString());
+        }
+
+        buffer.append(SUFFIX);
+
+        IResourceLocation localized = baseLocation.getRelativeLocation(buffer.toString());
+
+        URL propertiesURL = localized.getResourceURL();
+
+        if (propertiesURL == null)
+            return parent;
+
+        Properties result = null;
+
+        if (parent == null)
+            result = new Properties();
+        else
+            result = new Properties(parent);
+
+        try
+        {
+            InputStream input = propertiesURL.openStream();
+
+            result.load(input);
+
+            input.close();
+        }
+        catch (IOException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("ComponentPropertiesStore.unable-to-read-input", propertiesURL),
+                ex);
+        }
+
+        return result;
+    }
+
+    /**
+     *  Clears the cache of read properties files.
+     * 
+     **/
+
+    public void reset()
+    {
+        _cache.clear();
+    }
+
+    public IMessages getMessages(IComponent component)
+    {
+        return new ComponentMessages(
+            component.getPage().getLocale(),
+            getLocalizedProperties(component));
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/DefaultMonitorFactory.java b/tapestry-framework/src/org/apache/tapestry/engine/DefaultMonitorFactory.java
new file mode 100644
index 0000000..328524f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/DefaultMonitorFactory.java
@@ -0,0 +1,38 @@
+//  Copyright 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.tapestry.engine;
+
+import org.apache.tapestry.request.RequestContext;
+
+/**
+ * Implementation of {@link org.apache.tapestry.engine.IMonitorFactory}
+ * that returns the {@link org.apache.tapestry.engine.NullMonitor}.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ */
+public class DefaultMonitorFactory implements IMonitorFactory
+{
+	public static final IMonitorFactory SHARED = new DefaultMonitorFactory();
+	 
+    /**
+     * Returns {@link NullMonitor#SHARED}.
+     */
+    public IMonitor createMonitor(RequestContext context)
+    {
+        return NullMonitor.SHARED;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/DefaultScriptSource.java b/tapestry-framework/src/org/apache/tapestry/engine/DefaultScriptSource.java
new file mode 100644
index 0000000..38ba857
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/DefaultScriptSource.java
@@ -0,0 +1,106 @@
+//  Copyright 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.tapestry.engine;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.IScript;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.script.ScriptParser;
+import org.apache.tapestry.util.xml.DocumentParseException;
+
+/**
+ *  Provides basic access to scripts available on the classpath.  Scripts are cached in
+ *  memory once parsed.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.2
+ * 
+ **/
+
+public class DefaultScriptSource implements IScriptSource
+{
+    private IResourceResolver _resolver;
+
+    private Map _cache = new HashMap();
+
+    public DefaultScriptSource(IResourceResolver resolver)
+    {
+        _resolver = resolver;
+    }
+
+    public synchronized void reset()
+    {
+        _cache.clear();
+    }
+
+    public synchronized IScript getScript(IResourceLocation scriptLocation)
+    {
+        IScript result = (IScript) _cache.get(scriptLocation);
+
+        if (result != null)
+            return result;
+
+        result = parse(scriptLocation);
+
+        _cache.put(scriptLocation, result);
+
+        return result;
+    }
+
+    private IScript parse(IResourceLocation location)
+    {
+        ScriptParser parser = new ScriptParser(_resolver);
+
+        try
+        {
+            return parser.parse(location);
+        }
+        catch (DocumentParseException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("DefaultScriptSource.unable-to-parse-script", location),
+                ex);
+        }
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer("DefaultScriptSource@");
+        buffer.append(Integer.toHexString(hashCode()));
+
+        buffer.append('[');
+
+        if (_cache != null)
+        {
+            synchronized (_cache)
+            {
+                buffer.append(_cache.keySet());
+            }
+
+            buffer.append(", ");
+        }
+
+        buffer.append("]");
+
+        return buffer.toString();
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/DefaultSpecificationSource.java b/tapestry-framework/src/org/apache/tapestry/engine/DefaultSpecificationSource.java
new file mode 100644
index 0000000..96dd020
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/DefaultSpecificationSource.java
@@ -0,0 +1,352 @@
+//  Copyright 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.tapestry.engine;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.INamespace;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.parse.SpecificationParser;
+import org.apache.tapestry.resource.ClasspathResourceLocation;
+import org.apache.tapestry.spec.IApplicationSpecification;
+import org.apache.tapestry.spec.IComponentSpecification;
+import org.apache.tapestry.spec.ILibrarySpecification;
+import org.apache.tapestry.spec.LibrarySpecification;
+import org.apache.tapestry.util.IRenderDescription;
+import org.apache.tapestry.util.pool.Pool;
+import org.apache.tapestry.util.xml.DocumentParseException;
+
+/**
+ *  Default implementation of {@link ISpecificationSource} that
+ *  expects to use the normal class loader to locate component
+ *  specifications from within the classpath.
+ *
+ * <p>Caches specifications in memory forever, or until {@link #reset()} is invoked.
+ *
+ * <p>An instance of this class acts like a singleton and is shared by multiple sessions,
+ * so it must be threadsafe.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * 
+ **/
+
+public class DefaultSpecificationSource implements ISpecificationSource, IRenderDescription
+{
+    private static final Log LOG = LogFactory.getLog(DefaultSpecificationSource.class);
+
+    /**
+     *  Key used to get and store {@link SpecificationParser} instances
+     *  from the Pool.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    private static final String PARSER_POOL_KEY = "org.apache.tapestry.SpecificationParser";
+
+    private IResourceResolver _resolver;
+    private IApplicationSpecification _specification;
+
+    private INamespace _applicationNamespace;
+    private INamespace _frameworkNamespace;
+
+    /**
+     *  Contains previously parsed component specifications.
+     *
+     **/
+
+    private Map _componentCache = new HashMap();
+
+    /**
+     *  Contains previously parsed page specifications.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    private Map _pageCache = new HashMap();
+
+    /**
+     *  Contains previously parsed library specifications, keyed
+     *  on specification resource path.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    private Map _libraryCache = new HashMap();
+
+    /**
+     *  Contains {@link INamespace} instances, keyed on id (which will
+     *  be null for the application specification).
+     * 
+     **/
+
+    private Map _namespaceCache = new HashMap();
+
+    /**
+     *  Reference to the shared {@link org.apache.tapestry.util.pool.Pool}.
+     * 
+     *  @see org.apache.tapestry.IEngine#getPool()
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    private Pool _pool;
+
+    public DefaultSpecificationSource(
+        IResourceResolver resolver,
+        IApplicationSpecification specification,
+        Pool pool)
+    {
+        _resolver = resolver;
+        _specification = specification;
+        _pool = pool;
+    }
+
+    /**
+     *  Clears the specification cache.  This is used during debugging.
+     *
+     **/
+
+    public synchronized void reset()
+    {
+        _componentCache.clear();
+        _pageCache.clear();
+        _libraryCache.clear();
+        _namespaceCache.clear();
+
+        _applicationNamespace = null;
+        _frameworkNamespace = null;
+    }
+
+    protected IComponentSpecification parseSpecification(
+        IResourceLocation resourceLocation,
+        boolean asPage)
+    {
+        IComponentSpecification result = null;
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Parsing component specification " + resourceLocation);
+
+        SpecificationParser parser = getParser();
+
+        try
+        {
+            if (asPage)
+                result = parser.parsePageSpecification(resourceLocation);
+            else
+                result = parser.parseComponentSpecification(resourceLocation);
+        }
+        catch (DocumentParseException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "DefaultSpecificationSource.unable-to-parse-specification",
+                    resourceLocation),
+                ex);
+        }
+        finally
+        {
+            discardParser(parser);
+        }
+
+        return result;
+    }
+
+    protected ILibrarySpecification parseLibrarySpecification(IResourceLocation resourceLocation)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Parsing library specification " + resourceLocation);
+
+        try
+        {
+            return getParser().parseLibrarySpecification(resourceLocation);
+        }
+        catch (DocumentParseException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "DefaultSpecificationSource.unable-to-parse-specification",
+                    resourceLocation),
+                ex);
+        }
+
+    }
+
+    public synchronized String toString()
+    {
+        ToStringBuilder builder = new ToStringBuilder(this);
+
+        builder.append("applicationNamespace", _applicationNamespace);
+        builder.append("frameworkNamespace", _frameworkNamespace);
+        builder.append("specification", _specification);
+
+        return builder.toString();
+    }
+
+    /** @since 1.0.6 **/
+
+    public synchronized void renderDescription(IMarkupWriter writer)
+    {
+        writer.print("DefaultSpecificationSource[");
+
+        writeCacheDescription(writer, "page", _pageCache);
+        writer.beginEmpty("br");
+        writer.println();
+
+        writeCacheDescription(writer, "component", _componentCache);
+        writer.print("]");
+        writer.println();
+    }
+
+    private void writeCacheDescription(IMarkupWriter writer, String name, Map cache)
+    {
+        Set keySet = cache.keySet();
+
+        writer.print(Tapestry.size(keySet));
+        writer.print(" cached ");
+        writer.print(name);
+        writer.print(" specifications:");
+
+        boolean first = true;
+
+        Iterator i = keySet.iterator();
+        while (i.hasNext())
+        {
+            // The keys are now IResourceLocation instances
+
+            Object key = i.next();
+
+            if (first)
+            {
+                writer.begin("ul");
+                first = false;
+            }
+
+            writer.begin("li");
+            writer.print(key.toString());
+            writer.end();
+        }
+
+        if (!first)
+            writer.end(); // <ul>
+    }
+
+    /**
+     *  Gets a component specification.
+     * 
+     *  @param resourceLocation the complete resource path to the specification.
+     *  @throws ApplicationRuntimeException if the specification cannot be obtained.
+     * 
+     **/
+
+    public synchronized IComponentSpecification getComponentSpecification(IResourceLocation resourceLocation)
+    {
+        IComponentSpecification result =
+            (IComponentSpecification) _componentCache.get(resourceLocation);
+
+        if (result == null)
+        {
+            result = parseSpecification(resourceLocation, false);
+
+            _componentCache.put(resourceLocation, result);
+        }
+
+        return result;
+    }
+
+    public synchronized IComponentSpecification getPageSpecification(IResourceLocation resourceLocation)
+    {
+        IComponentSpecification result = (IComponentSpecification) _pageCache.get(resourceLocation);
+
+        if (result == null)
+        {
+            result = parseSpecification(resourceLocation, true);
+
+            _pageCache.put(resourceLocation, result);
+        }
+
+        return result;
+    }
+
+    public synchronized ILibrarySpecification getLibrarySpecification(IResourceLocation resourceLocation)
+    {
+        ILibrarySpecification result = (LibrarySpecification) _libraryCache.get(resourceLocation);
+
+        if (result == null)
+        {
+            result = parseLibrarySpecification(resourceLocation);
+            _libraryCache.put(resourceLocation, result);
+        }
+
+        return result;
+    }
+
+    /** @since 2.2 **/
+
+    protected SpecificationParser getParser()
+    {
+        SpecificationParser result = (SpecificationParser) _pool.retrieve(PARSER_POOL_KEY);
+
+        if (result == null)
+            result = new SpecificationParser(_resolver);
+
+        return result;
+    }
+
+    /** @since 3.0 **/
+
+    protected void discardParser(SpecificationParser parser)
+    {
+        _pool.store(PARSER_POOL_KEY, parser);
+    }
+
+    public synchronized INamespace getApplicationNamespace()
+    {
+        if (_applicationNamespace == null)
+            _applicationNamespace = new Namespace(null, null, _specification, this);
+
+        return _applicationNamespace;
+    }
+
+    public synchronized INamespace getFrameworkNamespace()
+    {
+        if (_frameworkNamespace == null)
+        {
+            IResourceLocation frameworkLocation =
+                new ClasspathResourceLocation(_resolver, "/org/apache/tapestry/Framework.library");
+
+            ILibrarySpecification ls = getLibrarySpecification(frameworkLocation);
+
+            _frameworkNamespace = new Namespace(INamespace.FRAMEWORK_NAMESPACE, null, ls, this);
+        }
+
+        return _frameworkNamespace;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/DefaultTemplateSource.java b/tapestry-framework/src/org/apache/tapestry/engine/DefaultTemplateSource.java
new file mode 100644
index 0000000..44765e1
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/DefaultTemplateSource.java
@@ -0,0 +1,628 @@
+//  Copyright 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.tapestry.engine;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IAsset;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.INamespace;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.parse.ComponentTemplate;
+import org.apache.tapestry.parse.ITemplateParserDelegate;
+import org.apache.tapestry.parse.TemplateParseException;
+import org.apache.tapestry.parse.TemplateParser;
+import org.apache.tapestry.parse.TemplateToken;
+import org.apache.tapestry.resolver.ComponentSpecificationResolver;
+import org.apache.tapestry.spec.IApplicationSpecification;
+import org.apache.tapestry.spec.IComponentSpecification;
+import org.apache.tapestry.util.DelegatingPropertySource;
+import org.apache.tapestry.util.IRenderDescription;
+import org.apache.tapestry.util.LocalizedPropertySource;
+import org.apache.tapestry.util.MultiKey;
+import org.apache.tapestry.util.PropertyHolderPropertySource;
+
+/**
+ *  Default implementation of {@link ITemplateSource}.  Templates, once parsed,
+ *  stay in memory until explicitly cleared.
+ *
+ *  <p>An instance of this class acts as a singleton shared by all sessions, so it
+ *  must be threadsafe.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class DefaultTemplateSource implements ITemplateSource, IRenderDescription
+{
+    private static final Log LOG = LogFactory.getLog(DefaultTemplateSource.class);
+
+
+    // The name of the component/application/etc property that will be used to
+    // determine the encoding to use when loading the template
+         
+    private static final String TEMPLATE_ENCODING_PROPERTY_NAME = "org.apache.tapestry.template-encoding"; 
+
+    // Cache of previously retrieved templates.  Key is a multi-key of 
+    // specification resource path and locale (local may be null), value
+    // is the ComponentTemplate.
+
+    private Map _cache = Collections.synchronizedMap(new HashMap());
+
+    // Previously read templates; key is the IResourceLocation, value
+    // is the ComponentTemplate.
+
+    private Map _templates = Collections.synchronizedMap(new HashMap());
+
+    /**
+     *  Number of tokens (each template contains multiple tokens).
+     *
+     **/
+
+    private int _tokenCount;
+
+    private static final int BUFFER_SIZE = 2000;
+
+    private TemplateParser _parser;
+
+    /** @since 2.2 **/
+
+    private IResourceLocation _applicationRootLocation;
+
+    /** @since 3.0 **/
+
+    private ITemplateSourceDelegate _delegate;
+
+    /**
+     *  Clears the template cache.  This is used during debugging.
+     *
+     **/
+
+    public void reset()
+    {
+        _cache.clear();
+        _templates.clear();
+
+        _tokenCount = 0;
+    }
+
+    /**
+     *  Reads the template for the component.
+     *
+     *  <p>Returns null if the template can't be found.
+     * 
+     **/
+
+    public ComponentTemplate getTemplate(IRequestCycle cycle, IComponent component)
+    {
+        IComponentSpecification specification = component.getSpecification();
+        IResourceLocation specificationLocation = specification.getSpecificationLocation();
+
+        Locale locale = component.getPage().getLocale();
+
+        Object key = new MultiKey(new Object[] { specificationLocation, locale }, false);
+
+        ComponentTemplate result = searchCache(key);
+        if (result != null)
+            return result;
+
+        result = findTemplate(cycle, specificationLocation, component, locale);
+
+        if (result == null)
+        {
+            result = getTemplateFromDelegate(cycle, component, locale);
+
+            if (result != null)
+                return result;
+
+            String stringKey =
+                component.getSpecification().isPageSpecification()
+                    ? "DefaultTemplateSource.no-template-for-page"
+                    : "DefaultTemplateSource.no-template-for-component";
+
+            throw new ApplicationRuntimeException(
+                Tapestry.format(stringKey, component.getExtendedId(), locale),
+                component,
+                component.getLocation(),
+                null);
+        }
+
+        saveToCache(key, result);
+
+        return result;
+    }
+
+    private ComponentTemplate searchCache(Object key)
+    {
+        return (ComponentTemplate) _cache.get(key);
+    }
+
+    private void saveToCache(Object key, ComponentTemplate template)
+    {
+        _cache.put(key, template);
+
+    }
+
+    private ComponentTemplate getTemplateFromDelegate(
+        IRequestCycle cycle,
+        IComponent component,
+        Locale locale)
+    {
+        if (_delegate == null)
+        {
+            IEngine engine = cycle.getEngine();
+            IApplicationSpecification spec = engine.getSpecification();
+
+            if (spec.checkExtension(Tapestry.TEMPLATE_SOURCE_DELEGATE_EXTENSION_NAME))
+                _delegate =
+                    (ITemplateSourceDelegate) spec.getExtension(
+                        Tapestry.TEMPLATE_SOURCE_DELEGATE_EXTENSION_NAME,
+                        ITemplateSourceDelegate.class);
+            else
+                _delegate = NullTemplateSourceDelegate.getSharedInstance();
+
+        }
+
+        return _delegate.findTemplate(cycle, component, locale);
+    }
+
+    /**
+     *  Finds the template for the given component, using the following rules:
+     *  <ul>
+     *  <li>If the component has a $template asset, use that
+     *  <li>Look for a template in the same folder as the component
+     *  <li>If a page in the application namespace, search in the application root
+     *  <li>Fail!
+     *  </ul>
+     * 
+     *  @return the template, or null if not found
+     * 
+     **/
+
+    private ComponentTemplate findTemplate(
+        IRequestCycle cycle,
+        IResourceLocation location,
+        IComponent component,
+        Locale locale)
+    {
+        IAsset templateAsset = component.getAsset(TEMPLATE_ASSET_NAME);
+
+        if (templateAsset != null)
+            return readTemplateFromAsset(cycle, component, templateAsset);
+
+        String name = location.getName();
+        int dotx = name.lastIndexOf('.');
+        String templateBaseName = name.substring(0, dotx + 1) + getTemplateExtension(component);
+
+        ComponentTemplate result =
+            findStandardTemplate(cycle, location, component, templateBaseName, locale);
+
+        if (result == null
+            && component.getSpecification().isPageSpecification()
+            && component.getNamespace().isApplicationNamespace())
+            result = findPageTemplateInApplicationRoot(cycle, component, templateBaseName, locale);
+
+        return result;
+    }
+
+    private ComponentTemplate findPageTemplateInApplicationRoot(
+        IRequestCycle cycle,
+        IComponent component,
+        String templateBaseName,
+        Locale locale)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Checking for " + templateBaseName + " in application root");
+
+        if (_applicationRootLocation == null)
+            _applicationRootLocation = Tapestry.getApplicationRootLocation(cycle);
+
+        IResourceLocation baseLocation =
+            _applicationRootLocation.getRelativeLocation(templateBaseName);
+        IResourceLocation localizedLocation = baseLocation.getLocalization(locale);
+
+        if (localizedLocation == null)
+            return null;
+
+        return getOrParseTemplate(cycle, localizedLocation, component);
+    }
+
+    /**
+     *  Reads an asset to get the template.
+     * 
+     **/
+
+    private ComponentTemplate readTemplateFromAsset(
+        IRequestCycle cycle,
+        IComponent component,
+        IAsset asset)
+    {
+        InputStream stream = asset.getResourceAsStream(cycle);
+
+        char[] templateData = null;
+
+        try
+        {
+            String encoding = getTemplateEncoding(cycle, component, null);
+            
+            templateData = readTemplateStream(stream, encoding);
+
+            stream.close();
+        }
+        catch (IOException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("DefaultTemplateSource.unable-to-read-template", asset),
+                ex);
+        }
+
+        IResourceLocation resourceLocation = asset.getResourceLocation();
+
+        return constructTemplateInstance(cycle, templateData, resourceLocation, component);
+    }
+
+    /**
+     *  Search for the template corresponding to the resource and the locale.
+     *  This may be in the template map already, or may involve reading and
+     *  parsing the template.
+     *
+     *  @return the template, or null if not found.
+     * 
+     **/
+
+    private ComponentTemplate findStandardTemplate(
+        IRequestCycle cycle,
+        IResourceLocation location,
+        IComponent component,
+        String templateBaseName,
+        Locale locale)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug(
+                "Searching for localized version of template for "
+                    + location
+                    + " in locale "
+                    + locale.getDisplayName());
+
+        IResourceLocation baseTemplateLocation = location.getRelativeLocation(templateBaseName);
+
+        IResourceLocation localizedTemplateLocation = baseTemplateLocation.getLocalization(locale);
+
+        if (localizedTemplateLocation == null)
+            return null;
+
+        return getOrParseTemplate(cycle, localizedTemplateLocation, component);
+
+    }
+
+    /**
+     *  Returns a previously parsed template at the specified location (which must already
+     *  be localized).  If not already in the template Map, then the
+     *  location is parsed and stored into the templates Map, then returned.
+     * 
+     **/
+
+    private ComponentTemplate getOrParseTemplate(
+        IRequestCycle cycle,
+        IResourceLocation location,
+        IComponent component)
+    {
+
+        ComponentTemplate result = (ComponentTemplate) _templates.get(location);
+        if (result != null)
+            return result;
+
+        // Ok, see if it exists.
+
+        result = parseTemplate(cycle, location, component);
+
+        if (result != null)
+            _templates.put(location, result);
+
+        return result;
+    }
+
+    /**
+     *  Reads the template for the given resource; returns null if the
+     *  resource doesn't exist.  Note that this method is only invoked
+     *  from a synchronized block, so there shouldn't be threading
+     *  issues here.
+     *
+     **/
+
+    private ComponentTemplate parseTemplate(
+        IRequestCycle cycle,
+        IResourceLocation location,
+        IComponent component)
+    {
+        String encoding = getTemplateEncoding(cycle, component, location.getLocale());
+        
+        char[] templateData = readTemplate(location, encoding);
+        if (templateData == null)
+            return null;
+
+        return constructTemplateInstance(cycle, templateData, location, component);
+    }
+
+    /** 
+     *  This method is currently synchronized, because
+     *  {@link TemplateParser} is not threadsafe.  Another good candidate
+     *  for a pooling mechanism, especially because parsing a template
+     *  may take a while.
+     * 
+     **/
+
+    private synchronized ComponentTemplate constructTemplateInstance(
+        IRequestCycle cycle,
+        char[] templateData,
+        IResourceLocation location,
+        IComponent component)
+    {
+        if (_parser == null)
+            _parser = new TemplateParser();
+
+        ITemplateParserDelegate delegate = new TemplateParserDelegateImpl(component, cycle);
+
+        TemplateToken[] tokens;
+
+        try
+        {
+            tokens = _parser.parse(templateData, delegate, location);
+        }
+        catch (TemplateParseException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("DefaultTemplateSource.unable-to-parse-template", location),
+                ex);
+        }
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Parsed " + tokens.length + " tokens from template");
+
+        _tokenCount += tokens.length;
+
+        return new ComponentTemplate(templateData, tokens);
+    }
+
+    /**
+     *  Reads the template, given the complete path to the
+     *  resource.  Returns null if the resource doesn't exist.
+     *
+     **/
+
+    private char[] readTemplate(IResourceLocation location, String encoding)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Reading template " + location);
+
+        URL url = location.getResourceURL();
+
+        if (url == null)
+        {
+            if (LOG.isDebugEnabled())
+                LOG.debug("Template does not exist.");
+
+            return null;
+        }
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Reading template from URL " + url);
+
+        InputStream stream = null;
+
+        try
+        {
+            stream = url.openStream();
+
+            return readTemplateStream(stream, encoding);
+        }
+        catch (IOException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("DefaultTemplateSource.unable-to-read-template", location),
+                ex);
+        }
+        finally
+        {
+            Tapestry.close(stream);
+        }
+
+    }
+
+    /**
+     *  Reads a Stream into memory as an array of characters.
+     *
+     **/
+
+    private char[] readTemplateStream(InputStream stream, String encoding) throws IOException
+    {
+        char[] charBuffer = new char[BUFFER_SIZE];
+        StringBuffer buffer = new StringBuffer();
+
+        InputStreamReader reader;
+        if (encoding != null)
+            reader = new InputStreamReader(new BufferedInputStream(stream), encoding);
+        else
+            reader = new InputStreamReader(new BufferedInputStream(stream));
+
+        try
+        {
+            while (true)
+            {
+                int charsRead = reader.read(charBuffer, 0, BUFFER_SIZE);
+
+                if (charsRead <= 0)
+                    break;
+
+                buffer.append(charBuffer, 0, charsRead);
+            }
+        }
+        finally
+        {
+            reader.close();
+        }
+
+        // OK, now reuse the charBuffer variable to
+        // produce the final result.
+
+        int length = buffer.length();
+
+        charBuffer = new char[length];
+
+        // Copy the character out of the StringBuffer and into the
+        // array.
+
+        buffer.getChars(0, length, charBuffer, 0);
+
+        return charBuffer;
+    }
+
+    public String toString()
+    {
+        ToStringBuilder builder = new ToStringBuilder(this);
+
+        builder.append("tokenCount", _tokenCount);
+
+        builder.append("templates", _templates.keySet());
+
+        return builder.toString();
+    }
+
+    /**
+     *  Checks for the {@link Tapestry#TEMPLATE_EXTENSION_PROPERTY} in the component's
+     *  specification, then in the component's namespace's specification.  Returns
+     *  {@link Tapestry#DEFAULT_TEMPLATE_EXTENSION} if not otherwise overriden.
+     * 
+     **/
+
+    private String getTemplateExtension(IComponent component)
+    {
+        String extension =
+            component.getSpecification().getProperty(Tapestry.TEMPLATE_EXTENSION_PROPERTY);
+
+        if (extension != null)
+            return extension;
+
+        extension =
+            component.getNamespace().getSpecification().getProperty(
+                Tapestry.TEMPLATE_EXTENSION_PROPERTY);
+
+        if (extension != null)
+            return extension;
+
+        return Tapestry.DEFAULT_TEMPLATE_EXTENSION;
+    }
+
+    /** @since 1.0.6 **/
+
+    public synchronized void renderDescription(IMarkupWriter writer)
+    {
+        writer.print("DefaultTemplateSource[");
+
+        if (_tokenCount > 0)
+        {
+            writer.print(_tokenCount);
+            writer.print(" tokens");
+        }
+
+        if (_cache != null)
+        {
+            boolean first = true;
+            Iterator i = _cache.entrySet().iterator();
+
+            while (i.hasNext())
+            {
+                if (first)
+                {
+                    writer.begin("ul");
+                    first = false;
+                }
+
+                Map.Entry e = (Map.Entry) i.next();
+                Object key = e.getKey();
+                ComponentTemplate template = (ComponentTemplate) e.getValue();
+
+                writer.begin("li");
+                writer.print(key.toString());
+                writer.print(" (");
+                writer.print(template.getTokenCount());
+                writer.print(" tokens)");
+                writer.println();
+                writer.end();
+            }
+
+            if (!first)
+            {
+                writer.end(); // <ul>
+                writer.beginEmpty("br");
+            }
+        }
+
+        writer.print("]");
+
+    }
+    
+    private String getTemplateEncoding(IRequestCycle cycle, IComponent component, Locale locale)
+    {
+        IPropertySource source = getComponentPropertySource(cycle, component);
+
+        if (locale != null)
+            source = new LocalizedPropertySource(locale, source);
+
+        return getTemplateEncodingProperty(source);
+    }
+    
+    private IPropertySource getComponentPropertySource(IRequestCycle cycle, IComponent component)
+    {
+        DelegatingPropertySource source = new DelegatingPropertySource();
+
+        // Search for the encoding property in the following order:
+        // First search the component specification
+        source.addSource(new PropertyHolderPropertySource(component.getSpecification()));
+
+        // Then search its library specification
+        source.addSource(new PropertyHolderPropertySource(component.getNamespace().getSpecification()));
+
+        // Then search the rest of the standard path
+        source.addSource(cycle.getEngine().getPropertySource());
+        
+        return source;
+    }
+    
+    private String getTemplateEncodingProperty(IPropertySource source)
+    {
+        return source.getPropertyValue(TEMPLATE_ENCODING_PROPERTY_NAME);
+    }
+    
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/DirectService.java b/tapestry-framework/src/org/apache/tapestry/engine/DirectService.java
new file mode 100644
index 0000000..ce77ac2
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/DirectService.java
@@ -0,0 +1,181 @@
+//  Copyright 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.tapestry.engine;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpSession;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IDirect;
+import org.apache.tapestry.IPage;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.StaleSessionException;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.request.RequestContext;
+import org.apache.tapestry.request.ResponseOutputStream;
+
+/**
+ *  Implementation of the direct service, which encodes the page and component id in
+ *  the service context, and passes application-defined parameters as well.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.9
+ *
+ **/
+
+public class DirectService extends AbstractService
+{
+    /**
+     *  Encoded into URL if engine was stateful.
+     * 
+     *  @since 3.0
+     **/
+
+    private static final String STATEFUL_ON = "1";
+
+    /**
+     *  Encoded into URL if engine was not stateful.
+     * 
+     *  @since 3.0
+     **/
+
+    private static final String STATEFUL_OFF = "0";
+
+    public ILink getLink(IRequestCycle cycle, IComponent component, Object[] parameters)
+    {
+
+        // New since 1.0.1, we use the component to determine
+        // the page, not the cycle.  Through the use of tricky
+        // things such as Block/InsertBlock, it is possible 
+        // that a component from a page different than
+        // the response page will render.
+        // In 1.0.6, we start to record *both* the render page
+        // and the component page (if different), as the extended
+        // context.
+
+        IPage renderPage = cycle.getPage();
+        IPage componentPage = component.getPage();
+
+        boolean complex = renderPage != componentPage;
+
+        String[] context = complex ? new String[4] : new String[3];
+
+        int i = 0;
+
+        String stateful = cycle.getEngine().isStateful() ? STATEFUL_ON : STATEFUL_OFF;
+
+        context[i++] = stateful;
+
+        if (complex)
+            context[i++] = renderPage.getPageName();
+
+        context[i++] = componentPage.getPageName();
+        context[i++] = component.getIdPath();
+
+        return constructLink(cycle, Tapestry.DIRECT_SERVICE, context, parameters, true);
+    }
+
+    public void service(
+        IEngineServiceView engine,
+        IRequestCycle cycle,
+        ResponseOutputStream output)
+        throws ServletException, IOException
+    {
+        IDirect direct;
+        int count = 0;
+        String componentPageName;
+        IPage componentPage;
+        RequestContext requestContext = cycle.getRequestContext();
+        String[] serviceContext = getServiceContext(requestContext);
+
+        if (serviceContext != null)
+            count = serviceContext.length;
+
+        if (count != 3 && count != 4)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("DirectService.context-parameters"));
+
+        boolean complex = count == 4;
+
+        int i = 0;
+        String stateful = serviceContext[i++];
+        String pageName = serviceContext[i++];
+
+        if (complex)
+            componentPageName = serviceContext[i++];
+        else
+            componentPageName = pageName;
+
+        String componentPath = serviceContext[i++];
+
+        IPage page = cycle.getPage(pageName);
+
+        cycle.activate(page);
+
+        if (complex)
+            componentPage = cycle.getPage(componentPageName);
+        else
+            componentPage = page;
+
+        IComponent component = componentPage.getNestedComponent(componentPath);
+
+        try
+        {
+            direct = (IDirect) component;
+        }
+        catch (ClassCastException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("DirectService.component-wrong-type", component.getExtendedId()),
+                component,
+                null,
+                ex);
+        }
+
+        // Check for a StateSession only the session was stateful when
+        // the Gesture was created.
+
+        if (stateful.equals(STATEFUL_ON) && direct.isStateful())
+        {
+            HttpSession session = cycle.getRequestContext().getSession();
+
+            if (session == null || session.isNew())
+                throw new StaleSessionException(
+                    Tapestry.format(
+                        "DirectService.stale-session-exception",
+                        direct.getExtendedId()),
+                    direct.getPage());
+        }
+
+        Object[] parameters = getParameters(cycle);
+
+        cycle.setServiceParameters(parameters);
+        direct.trigger(cycle);
+
+        // Render the response.  This will be the response page (the first element in the context)
+        // unless the direct (or its delegate) changes it.
+
+        engine.renderResponse(cycle, output);
+    }
+
+    public String getName()
+    {
+        return Tapestry.DIRECT_SERVICE;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/EngineServiceLink.java b/tapestry-framework/src/org/apache/tapestry/engine/EngineServiceLink.java
new file mode 100644
index 0000000..c766048
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/EngineServiceLink.java
@@ -0,0 +1,250 @@
+//  Copyright 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.tapestry.engine;
+
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.codec.net.URLCodec;
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.request.RequestContext;
+
+/**
+ *  A EngineServiceLink represents a possible action within the client web browser;
+ *  either clicking a link or submitting a form, which is constructed primarily
+ *  from the {@link org.apache.tapestry.IEngine#getServletPath() servlet path},
+ *  with some additional query parameters.  A full URL for the EngineServiceLink
+ *  can be generated, or the query parameters for the EngineServiceLink can be extracted
+ *  (separately from the servlet path).  The latter case is used when submitting
+ *  constructing {@link org.apache.tapestry.form.Form forms}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ * 
+ **/
+
+public class EngineServiceLink implements ILink
+{
+    private static final int DEFAULT_HTTP_PORT = 80;
+    private static final URLCodec _urlCodec = new URLCodec();
+
+    private IRequestCycle _cycle;
+    private String _service;
+    private String[] _parameters;
+    private boolean _stateful;
+
+    /**
+     *  Creates a new EngineServiceLink.  A EngineServiceLink always names a service to be activated
+     *  by the link, has an optional list of service context strings,
+     *  an optional list of service parameter strings and may be stateful
+     *  or stateless.
+     * 
+     *  <p>ServiceLink parameter strings may contain any characters.
+     * 
+     *  <p>ServiceLink context strings must be URL safe, and may not contain
+     *  slash ('/') characters.  Typically, only letters, numbers and simple
+     *  punctuation ('.', '-', '_', ':') is recommended (no checks are currently made,
+     *  however).  Context strings are generally built from page names
+     *  and component ids, which are limited to safe characters.
+     *  
+     *  @param cycle The {@link IRequestCycle} the EngineServiceLink is to be created for.
+     *  @param serviceName The name of the service to be invoked by the EngineServiceLink.
+     *  @param serviceContext an optional array of strings to be provided
+     *  to the service to provide a context for executing the service.  May be null
+     *  or empty.  <b>Note: copied, not retained.</b>
+     *  @param serviceParameters An array of parameters, may be 
+     *  null or empty. <b>Note: retained, not copied.</b>
+     *  @param stateful if true, the service which generated the EngineServiceLink
+     *  is stateful and expects that the final URL will be passed through
+     *  {@link IRequestCycle#encodeURL(String)}.
+     **/
+
+    public EngineServiceLink(
+        IRequestCycle cycle,
+        String serviceName,
+        String[] serviceContext,
+        String[] serviceParameters,
+        boolean stateful)
+    {
+        _cycle = cycle;
+        _service = constructServiceValue(serviceName, serviceContext);
+        _parameters = serviceParameters;
+        _stateful = stateful;
+    }
+
+    private String constructServiceValue(String serviceName, String[] serviceContext)
+    {
+        int count = Tapestry.size(serviceContext);
+
+        if (count == 0)
+            return serviceName;
+
+        StringBuffer buffer = new StringBuffer(serviceName);
+
+        for (int i = 0; i < count; i++)
+        {
+            buffer.append('/');
+
+            buffer.append(serviceContext[i]);
+        }
+
+        return buffer.toString();
+    }
+
+    public String getURL()
+    {
+        return getURL(null, true);
+    }
+
+    public String getURL(String anchor, boolean includeParameters)
+    {
+        return constructURL(new StringBuffer(), anchor, includeParameters);
+    }
+
+    public String getAbsoluteURL()
+    {
+        return getAbsoluteURL(null, null, 0, null, true);
+    }
+
+    public String getAbsoluteURL(
+        String scheme,
+        String server,
+        int port,
+        String anchor,
+        boolean includeParameters)
+    {
+        StringBuffer buffer = new StringBuffer();
+        RequestContext context = _cycle.getRequestContext();
+
+        if (scheme == null)
+            scheme = context.getScheme();
+
+        buffer.append(scheme);
+        buffer.append("://");
+
+        if (server == null)
+            server = context.getServerName();
+
+        buffer.append(server);
+
+        if (port == 0)
+            port = context.getServerPort();
+
+        if (!(scheme.equals("http") && port == DEFAULT_HTTP_PORT))
+        {
+            buffer.append(':');
+            buffer.append(port);
+        }
+
+        // Add the servlet path and the rest of the URL & query parameters.
+        // The servlet path starts with a leading slash.
+
+        return constructURL(buffer, anchor, includeParameters);
+    }
+
+    private String constructURL(StringBuffer buffer, String anchor, boolean includeParameters)
+    {
+        buffer.append(_cycle.getEngine().getServletPath());
+
+        if (includeParameters)
+        {
+            buffer.append('?');
+            buffer.append(Tapestry.SERVICE_QUERY_PARAMETER_NAME);
+            buffer.append('=');
+            buffer.append(_service);
+
+            int count = Tapestry.size(_parameters);
+
+            for (int i = 0; i < count; i++)
+            {
+                buffer.append('&');
+
+                buffer.append(Tapestry.PARAMETERS_QUERY_PARAMETER_NAME);
+                buffer.append('=');
+
+                String encoding = _cycle.getEngine().getOutputEncoding();
+                try
+                {
+                    String encoded = _urlCodec.encode(_parameters[i], encoding);
+                    buffer.append(encoded);
+                }
+                catch (UnsupportedEncodingException e)
+                {
+                    throw new ApplicationRuntimeException(
+                        Tapestry.format("illegal-encoding", encoding),
+                        e);
+                }
+            }
+        }
+
+        if (anchor != null)
+        {
+            buffer.append('#');
+            buffer.append(anchor);
+        }
+
+        String result = buffer.toString();
+
+        if (_stateful)
+            result = _cycle.encodeURL(result);
+
+        return result;
+    }
+
+    public String[] getParameterNames()
+    {
+        List list = new ArrayList();
+
+        list.add(Tapestry.SERVICE_QUERY_PARAMETER_NAME);
+
+        if (Tapestry.size(_parameters) != 0)
+            list.add(Tapestry.PARAMETERS_QUERY_PARAMETER_NAME);
+
+        return (String[]) list.toArray(new String[list.size()]);
+    }
+
+    public String[] getParameterValues(String name)
+    {
+        if (name.equals(Tapestry.SERVICE_QUERY_PARAMETER_NAME))
+        {
+            return new String[] { _service };
+        }
+
+        if (name.equals(Tapestry.PARAMETERS_QUERY_PARAMETER_NAME))
+        {
+            return _parameters;
+        }
+
+        throw new IllegalArgumentException(
+            Tapestry.format("EngineServiceLink.unknown-parameter-name", name));
+    }
+
+    public String toString()
+    {
+        ToStringBuilder builder = new ToStringBuilder(this);
+
+        builder.append("service", _service);
+        builder.append("parameters", _parameters);
+        builder.append("stateful", _stateful);
+
+        return builder.toString();
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/ExternalService.java b/tapestry-framework/src/org/apache/tapestry/engine/ExternalService.java
new file mode 100644
index 0000000..9e0790f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/ExternalService.java
@@ -0,0 +1,185 @@
+//  Copyright 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.tapestry.engine;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IExternalPage;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.request.ResponseOutputStream;
+
+/**
+ * The external service enables external applications
+ * to reference Tapestry pages via a URL. Pages which can be referenced
+ * by the external service must implement the {@link IExternalPage}
+ * interface. The external service enables the bookmarking of pages.
+ * 
+ * <p>
+ * The external service may also be used by the Tapestry JSP taglibrary
+ * ({@link org.apache.tapestry.jsp.ExternalURLTag} and {@link org.apache.tapestry.jsp.ExternalTag}).
+ * 
+ * <p> 
+ * You can try and second guess the URL format used by Tapestry. 
+ * The default URL format for the external service is:
+ * <blockquote>
+ * <tt>http://localhost/app?service=external/<i>[Page Name]</i>&amp;sp=[Param 0]&amp;sp=[Param 1]...</tt>
+ * </blockquote>
+ * For example to view the "ViewCustomer" page the service parameters 5056 (customer ID) and
+ * 309 (company ID) the external service URL would be:
+ * <blockquote>
+ * <tt>http://localhost/myapp?service=external&amp;context=<b>ViewCustomer</b>&amp;sp=<b>5056</b>&amp;sp=<b>302</b></tt>
+ * </blockquote>
+ * In this example external service will get a "ViewCustomer" page and invoke the 
+ * {@link IExternalPage#activateExternalPage(Object[], IRequestCycle)} method with the parameters:  
+ * Object[] { new Integer(5056), new Integer(302) }.
+ * <p>
+ * Note service parameters (sp) need to be prefixed by valid
+ * {@link org.apache.tapestry.util.io.DataSqueezer} adaptor char. These adaptor chars are automatically provided in
+ * URL's created by the <tt>buildGesture()</tt> method. However if you hand coded an external 
+ * service URL you will need to ensure valid prefix chars are present.
+ * <p>
+ * <table border="1" cellpadding="2">
+ *  <tr>
+ *   <th>Prefix char(s)</th><th>Mapped Java Type</th>
+ *  </tr>
+ *  <tr>
+ *   <td>&nbsp;TF</td><td>&nbsp;boolean</td>
+ *  </tr>
+ *  <tr>
+ *   <td>&nbsp;b</td><td>&nbsp;byte</td>
+ *  </tr>
+ *  <tr>
+ *   <td>&nbsp;c</td><td>&nbsp;char</td>
+ *  </tr>
+ *  <tr>
+ *   <td>&nbsp;d</td><td>&nbsp;double</td>
+ *  </tr>
+ *  <tr>
+ *   <td>&nbsp;-0123456789</td><td>&nbsp;integer</td>
+ *  </tr>
+ *  <tr>
+ *   <td>&nbsp;l</td><td>&nbsp;long</td>
+ *  </tr>
+ *  <tr>
+ *   <td>&nbsp;S</td><td>&nbsp;String</td>
+ *  </tr>
+ *  <tr>
+ *   <td>&nbsp;s</td><td>&nbsp;short</td>
+ *  </tr>
+ *  <tr>
+ *   <td>&nbsp;other chars</td>
+ *   <td>&nbsp;<tt>String</tt> without truncation of first char</td>
+ *  </tr>
+ * <table>
+ *  <p>
+ *  <p>
+ *  A good rule of thumb is to keep the information encoded in the URL short and simple, and restrict it
+ *  to just Strings and Integers.  Integers can be encoded as-is.  Prefixing all Strings with the letter 'S'
+ *  will ensure that they are decoded properly.  Again, this is only relevant if an 
+ *  {@link org.apache.tapestry.IExternalPage} is being referenced from static HTML or JSP and the
+ *  URL must be assembled in user code ... when the URL is generated by Tapestry, it is automatically
+ *  created with the correct prefixes and encodings (as with any other service).
+ * 
+ * @see org.apache.tapestry.IExternalPage
+ * @see org.apache.tapestry.jsp.ExternalTag
+ * @see org.apache.tapestry.jsp.ExternalURLTag
+ *
+ * @author Howard Lewis Ship
+ * @author Malcolm Edgar
+ * @since 2.2
+ *  
+ **/
+
+public class ExternalService extends AbstractService
+{
+
+  /**
+   *  Builds a URL for a service.  This is performed during the
+   *  rendering phase of one request cycle and bulds URLs that will
+   *  invoke activity in a subsequent request cycle.
+   *
+   *  @param cycle Defines the request cycle being processed.
+   *  @param component The component requesting the URL.  Generally, the
+   *  service context is established from the component.
+   *  @param parameters Additional parameters specific to the
+   *  component requesting the EngineServiceLink.
+   *  @return The URL for the service.  The URL always be encoded when it is returned.
+   *
+   **/
+  
+    public ILink getLink(IRequestCycle cycle, IComponent component, Object[] parameters)
+    {
+        if (parameters == null || parameters.length == 0)
+            throw new ApplicationRuntimeException(
+                Tapestry.format("service-requires-parameters", Tapestry.EXTERNAL_SERVICE));
+
+        String pageName = (String) parameters[0];
+        String[] context = new String[] { pageName };
+
+        Object[] pageParameters = new Object[parameters.length - 1];
+        System.arraycopy(parameters, 1, pageParameters, 0, parameters.length - 1);
+
+        return constructLink(cycle, Tapestry.EXTERNAL_SERVICE, context, pageParameters, true);
+    }
+
+    public void service(
+        IEngineServiceView engine,
+        IRequestCycle cycle,
+        ResponseOutputStream output)
+        throws ServletException, IOException
+    {
+        IExternalPage page = null;
+
+        String[] context = getServiceContext(cycle.getRequestContext());
+
+        if (context == null || context.length != 1)
+            throw new ApplicationRuntimeException(
+                Tapestry.format("service-single-context-parameter", Tapestry.EXTERNAL_SERVICE));
+
+        String pageName = context[0];
+
+        try
+        {
+            page = (IExternalPage) cycle.getPage(pageName);
+        }
+        catch (ClassCastException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("ExternalService.page-not-compatible", pageName),
+                ex);
+        }
+
+        Object[] parameters = getParameters(cycle);
+
+        cycle.setServiceParameters(parameters);
+
+        cycle.activate(page);
+
+        page.activateExternalPage(parameters, cycle);
+
+        // Render the response.
+        engine.renderResponse(cycle, output);
+    }
+
+    public String getName()
+    {
+        return Tapestry.EXTERNAL_SERVICE;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/HomeService.java b/tapestry-framework/src/org/apache/tapestry/engine/HomeService.java
new file mode 100644
index 0000000..50d6c33
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/HomeService.java
@@ -0,0 +1,71 @@
+//  Copyright 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.tapestry.engine;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.IPage;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.request.ResponseOutputStream;
+
+/**
+ *  An implementation of the home service that renders the Home page.
+ *  This is the most likely candidate for overriding ... for example,
+ *  to select the page to render based on known information about the
+ *  user (stored as a cookie).
+ * 
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.9
+ *
+ **/
+
+public class HomeService extends AbstractService
+{
+
+    public ILink getLink(IRequestCycle cycle, IComponent component, Object[] parameters)
+    {
+        if (Tapestry.size(parameters) != 0)
+            throw new IllegalArgumentException(
+                Tapestry.format("service-no-parameters", Tapestry.HOME_SERVICE));
+
+        return constructLink(cycle, Tapestry.HOME_SERVICE, null, null, true);
+    }
+
+    public void service(
+        IEngineServiceView engine,
+        IRequestCycle cycle,
+        ResponseOutputStream output)
+        throws ServletException, IOException
+    {
+        IPage home = cycle.getPage(IEngine.HOME_PAGE);
+
+        cycle.activate(home);
+
+        engine.renderResponse(cycle, output);
+    }
+
+    public String getName()
+    {
+        return Tapestry.HOME_SERVICE;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/IComponentClassEnhancer.java b/tapestry-framework/src/org/apache/tapestry/engine/IComponentClassEnhancer.java
new file mode 100644
index 0000000..03fe4d2
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/IComponentClassEnhancer.java
@@ -0,0 +1,60 @@
+//  Copyright 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.tapestry.engine;
+
+import org.apache.tapestry.spec.IComponentSpecification;
+
+/**
+ *
+ *  A provider of enhanced classes, classes with new methods 
+ *  and new attributes, and possibly, implementing new
+ *  Java interfaces.  The primary use of class enhancement is to
+ *  automate the creation of transient and persistant properties.
+ * 
+ *  <p>
+ *  Implementations of this interface must be threadsafe.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ * 
+ **/
+
+public interface IComponentClassEnhancer
+{
+	/**
+	 *  Clears all cached data for the enhancer; this includes references to
+	 *  enhanced classes.
+	 * 
+	 **/
+	
+	public void reset();
+	
+	/**
+	 *  Used to access the class for a given component (or page).  Returns the
+	 *  specified class, or an enhanced version of the class if the
+	 *  component requires enhancement.
+	 * 
+	 *  @param specification the specification for the component
+	 *  @param className the name of base class to enhance, as extracted
+	 *  from the specification (or possibly, from a default).
+	 * 
+	 *  @throws org.apache.tapestry.ApplicationRuntimeException if the class does not exist, is invalid,
+	 *  or may not be enhanced.
+	 * 
+	 **/
+	
+	public Class getEnhancedClass(IComponentSpecification specification, String className);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/IComponentMessagesSource.java b/tapestry-framework/src/org/apache/tapestry/engine/IComponentMessagesSource.java
new file mode 100644
index 0000000..ce6e061
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/IComponentMessagesSource.java
@@ -0,0 +1,40 @@
+//  Copyright 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.tapestry.engine;
+
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IMessages;
+
+/**
+ *  Defines an object that can provide a component with its
+ *  {@link org.apache.tapestry.IMessages}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.0.4
+ *
+ **/
+
+public interface IComponentMessagesSource
+{
+	public IMessages getMessages(IComponent component);
+	
+	/**
+	 *  Clears all cached information for the source.
+	 * 
+	 **/
+	
+	public void reset();
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/IEngineService.java b/tapestry-framework/src/org/apache/tapestry/engine/IEngineService.java
new file mode 100644
index 0000000..62d7a15
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/IEngineService.java
@@ -0,0 +1,85 @@
+//  Copyright 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.tapestry.engine;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.request.ResponseOutputStream;
+
+/**
+ *  A service, provided by the {@link org.apache.tapestry.IEngine}, for its pages and/or components.
+ *  Services are
+ *  responsible for constructing {@link EngineServiceLink}s (an encoding of URLs)
+ *  to represent dynamic application behavior, and for
+ *  parsing those URLs when a subsequent request involves them.
+ *
+ *  @see org.apache.tapestry.IEngine#getService(String)
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public interface IEngineService
+{
+    /**
+     *  Builds a URL for a service.  This is performed during the
+     *  rendering phase of one request cycle and bulds URLs that will
+     *  invoke activity in a subsequent request cycle.
+     *
+     *  @param cycle Defines the request cycle being processed.
+     *  @param component The component requesting the URL.  Generally, the
+     *  service context is established from the component.
+     *  @param parameters Additional parameters specific to the
+     *  component requesting the EngineServiceLink.
+     *  @return The URL for the service.  The URL will have to be encoded
+     *  via {@link javax.servlet.http.HttpServletResponse#encodeURL(java.lang.String)}.
+     *
+     **/
+
+    public ILink getLink(IRequestCycle cycle, IComponent component, Object[] parameters);
+
+    /**
+     *  Perform the service, interpreting the URL (from the
+     *  {@link javax.servlet.http.HttpServletRequest}) 
+     *  responding appropriately, and
+     *  rendering a result page.
+     *
+     *
+     *  @see org.apache.tapestry.IEngine#service(org.apache.tapestry.request.RequestContext)
+     *  @param engine a view of the {@link org.apache.tapestry.IEngine} with additional methods needed by services
+     *  @param cycle the incoming request
+     *  @param output stream to which output should ultimately be directed
+     * 
+     **/
+
+    public void service(
+        IEngineServiceView engine,
+        IRequestCycle cycle,
+        ResponseOutputStream output)
+        throws ServletException, IOException;
+
+    /**
+     *  Returns the name of the service.
+     *
+     *  @since 1.0.1
+     **/
+
+    public String getName();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/IEngineServiceView.java b/tapestry-framework/src/org/apache/tapestry/engine/IEngineServiceView.java
new file mode 100644
index 0000000..9a612e8
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/IEngineServiceView.java
@@ -0,0 +1,78 @@
+//  Copyright 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.tapestry.engine;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.request.ResponseOutputStream;
+
+/**
+ *  Additional methods implemented by the engine that are 
+ *  exposed to {@link IEngineService engine services}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.9
+ *
+ */
+
+public interface IEngineServiceView extends IEngine
+{
+    /**
+     *  Invoked by a service to force the page selected by the {@link IRequestCycle}
+     *  to be renderred.  This takes care of a number of bookkeeping issues, such
+     *  as committing changes in page recorders.
+     * 
+     **/
+
+    public void renderResponse(IRequestCycle cycle, ResponseOutputStream output)
+        throws ServletException, IOException;
+
+    /**
+     *  Invoked to restart the application from start; this most frequently follows
+     *  some kind of catastrophic failure.  This will invalidate any {@link javax.servlet.http.HttpSession}
+     *  and force a redirect to the application servlet (i.e., invoking the home service
+     *  in a subsequent request cycle).
+     * 
+     **/
+
+    public void restart(IRequestCycle cycle) throws IOException;
+
+    /**
+     *  Invoked (typically by the reset service) to clear all cached data known
+     *  to the engine.  This includes 
+     *  pages, templates, helper beans, specifications,
+     *  localized strings, etc., and
+     *  is used during debugging.
+     * 
+     **/
+
+    public void clearCachedData();
+
+    /**
+     *  Writes a detailed report of the exception to <code>System.err</code>.
+     *  This is invoked by services that can't write an HTML description
+     *  of the error because they don't provide text/html content (such as
+     *  an asset that creates an image).
+     *
+     *  @since 1.0.10
+     */
+
+    public void reportException(String reportTitle, Throwable ex);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/ILink.java b/tapestry-framework/src/org/apache/tapestry/engine/ILink.java
new file mode 100644
index 0000000..67f6a21
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/ILink.java
@@ -0,0 +1,111 @@
+//  Copyright 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.tapestry.engine;
+
+/**
+ *  Define a link that may be generated as part of a page render.  The vast majority
+ *  of links are tied to {@link IEngineService services} and are, in
+ *  fact, callbacks.  A small number, such as those generated by
+ *  {@link org.apache.tapestry.link.GenericLink} component, are to arbitrary locations.
+ *  In addition, ILink differentiates between the path portion of the link, and any
+ *  query parameters encoded into a link, primarily to benefit {@link org.apache.tapestry.form.Form},
+ *  which needs to encode the query parameters as hidden form fields.
+ *
+ *  <p>
+ *  In addition, an ILink is responsible for
+ *  passing constructed URLs through
+ *  {@link org.apache.tapestry.IRequestCycle#encodeURL(String)}
+ *  as needed.
+ * 
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ * 
+ **/
+
+public interface ILink
+{
+	/**
+	 *  Returns the relative URL as a String.  A relative
+	 *  URL may include a leading slash, but omits
+	 *  the scheme, host and port portions of a full URL.
+	 *  
+	 *  @return the relative URL, with no anchor, but including
+	 *  query parameters.
+	 * 
+	 **/
+	
+	public String getURL();
+	
+    /**
+     *  Returns the relative URL as a String.  This is used
+     *  for most links.
+     * 
+     *  @param anchor if not null, appended to the URL
+     *  @param includeParameters if true, parameters are included
+     * 
+     **/
+
+    public String getURL(String anchor, boolean includeParameters);
+
+	/**
+	 *  Returns the absolute URL as a String, using
+	 *  default scheme, server and port, including
+	 *  parameters, and no anchor.
+	 * 
+	 **/
+	
+	public String getAbsoluteURL();
+
+    /**
+     *  Returns the absolute URL as a String.  
+     * 
+     *  @param scheme if not null, overrides the default scheme.
+     *  @param server if not null, overrides the default server
+     *  @param port if non-zero, overrides the default port
+     *  @param anchor if not null, appended to the URL
+     *  @param includeParameters if true, parameters are included
+     * 
+     **/
+
+    public String getAbsoluteURL(
+        String scheme,
+        String server,
+        int port,
+        String anchor,
+        boolean includeParameters);
+
+    /**
+     * 
+     *  Returns an array of parameters names (in
+     *  no specified order).
+     * 
+     *  @see #getParameterValues(String)
+     * 
+     **/
+
+    public String[] getParameterNames();
+
+    /**
+     *  Returns the values for the named parameter.
+     *  
+     *  @throws IllegalArgumentException if the
+     *  link does not define values for the
+     *  specified name.
+     * 
+     **/
+
+    public String[] getParameterValues(String name);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/IMonitor.java b/tapestry-framework/src/org/apache/tapestry/engine/IMonitor.java
new file mode 100644
index 0000000..259db5b
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/IMonitor.java
@@ -0,0 +1,121 @@
+//  Copyright 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.tapestry.engine;
+
+/**
+ *  Basic support for application monitoring and metrics.  
+ *  This interface defines events; the implementation
+ *  decides what to do with them (such as record them to a database).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public interface IMonitor
+{
+	/**
+	 *  Invoked before constructing a page.
+	 *
+	 **/
+
+	public void pageCreateBegin(String pageName);
+
+	/**
+	 *  Invoked after successfully constructing a page and all of its components.
+	 *
+	 **/
+
+	public void pageCreateEnd(String pageName);
+
+	/**
+	 *  Invoked when a page is loaded.  This includes time to locate or create an instance
+	 *  of the page and rollback its state (to any previously recorded value).
+	 *
+	 **/
+
+	public void pageLoadBegin(String pageName);
+
+	/**
+	 *  Invoked once a page is completely loaded and rolled back to its prior state.
+	 *
+	 **/
+
+	public void pageLoadEnd(String pageName);
+
+	/**
+	 *  Invoked before a page render begins.
+	 *
+	 **/
+
+	public void pageRenderBegin(String pageName);
+
+	/**
+	 *  Invoked after a page has succesfully rendered.
+	 *
+	 **/
+
+	public void pageRenderEnd(String pageName);
+
+	/**
+	 *  Invoked before a page rewind (to respond to an action) begins.
+	 *
+	 **/
+
+	public void pageRewindBegin(String pageName);
+
+	/**
+	 *  Invoked after a page has succesfully been rewound (which includes
+	 *  any activity related to the action listener).
+	 *
+	 **/
+
+	public void pageRewindEnd(String pageName);
+
+	/**
+	 *  Invoked when a service begins processing.
+	 *
+	 **/
+
+	public void serviceBegin(String serviceName, String detailMessage);
+
+	/**
+	 *  Invoked when a service successfully ends.
+	 *
+	 **/
+
+	public void serviceEnd(String serviceName);
+
+	/**
+	 *  Invoked when a service throws an exception rather than completing normally.
+	 *  Processing of the request may continue with the display of an exception
+	 *  page.
+	 * 
+	 *  <p>
+	 *  serviceException() is always invoked <em>before</em>
+	 * {@link #serviceEnd(String)}.
+	 *
+	 **/
+
+	public void serviceException(Throwable exception);
+
+	/**
+	 *  Invoked when a session is initiated.  This is typically
+	 *  done from the implementation of the home service.
+	 *
+	 **/
+
+	public void sessionBegin();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/IMonitorFactory.java b/tapestry-framework/src/org/apache/tapestry/engine/IMonitorFactory.java
new file mode 100644
index 0000000..cb7f5a3
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/IMonitorFactory.java
@@ -0,0 +1,41 @@
+//  Copyright 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.tapestry.engine;
+
+import org.apache.tapestry.request.RequestContext;
+
+/**
+ * Interface for an object that can create a {@link IMonitor} instance
+ * for a particular {@link org.apache.tapestry.request.RequestContext}.
+ * The engine expects there to be a monitor factory
+ * as application extension
+ * <code>org.apache.tapestry.monitor-factory</code>.  If no such
+ * extension exists, then {@link org.apache.tapestry.engine.DefaultMonitorFactory}
+ * is used instead.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * @since 3.0
+ */
+public interface IMonitorFactory
+{
+    /**
+     * Create a new {@link IMonitor} instance.  Alternately, return a shared instance.
+     * This method may be invoked by multiple threads.
+     * 
+     */
+
+    public IMonitor createMonitor(RequestContext context);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/IPageLoader.java b/tapestry-framework/src/org/apache/tapestry/engine/IPageLoader.java
new file mode 100644
index 0000000..eeb1226
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/IPageLoader.java
@@ -0,0 +1,70 @@
+//  Copyright 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.tapestry.engine;
+
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ * Interface exposed to components as they are loaded by the page loader.
+ *
+ * @see IComponent#finishLoad(IRequestCycle, IPageLoader, org.apache.tapestry.spec.IComponentSpecification)
+ * 
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * 
+ **/
+
+public interface IPageLoader
+{
+    /**
+     *  Returns the engine for which this page loader is curently
+     *  constructing a page.
+     *
+     *  @since 0.2.12
+     * 
+     **/
+
+    public IEngine getEngine();
+
+    /**
+     *  A convienience; returns the template source provided by
+     *  the {@link IEngine engine}.
+     *
+     *  @since 0.2.12
+     * 
+     **/
+
+    public ITemplateSource getTemplateSource();
+
+    /**
+     *  Invoked to create an implicit component (one which is defined in the
+     *  containing component's template, rather that in the containing component's
+     *  specification).
+     * 
+     *  @see org.apache.tapestry.BaseComponentTemplateLoader
+     *  @since 3.0
+     * 
+     **/
+
+    public IComponent createImplicitComponent(
+        IRequestCycle cycle,
+        IComponent container,
+        String componentId,
+        String componentType,
+        ILocation location);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/IPageRecorder.java b/tapestry-framework/src/org/apache/tapestry/engine/IPageRecorder.java
new file mode 100644
index 0000000..60f6bbe
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/IPageRecorder.java
@@ -0,0 +1,141 @@
+//  Copyright 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.tapestry.engine;
+
+import java.util.Collection;
+
+import org.apache.tapestry.IPage;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.event.ChangeObserver;
+
+/**
+ *  Defines an object that can observe changes to properties of
+ *  a page and its components, store the state of the page between request cycles,
+ *  and restore a page's state on a subsequent request cycle.
+ *
+ *  <p>Concrete implementations of this can store the changes in memory,
+ *  as client-side cookies, in a flat file, or in a database.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public interface IPageRecorder extends ChangeObserver
+{
+    /**
+     *  Invoked after the recorder is instantiated to initialize
+     *  it for the current request cycle.
+     * 
+     *  @param pageName the fully qualified page name
+     *  @param cycle the current request cycle
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    public void initialize(String pageName, IRequestCycle cycle);
+
+    /**
+     *  Invoked at the end of a request cycle in which the
+     *  page recorder is discarded (either implicitly, because
+     *  the page recorder has no changes, or explicitly
+     *  because of {@link org.apache.tapestry.IEngine#forgetPage(String)} or
+     *  {@link #markForDiscard()}.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    public void discard();
+
+    /**
+     *  Persists all changes that have been accumulated.  If the recorder
+     *  saves change incrementally, this should ensure that all changes have been persisted.
+     *
+     *  <p>After commiting, a page recorder automatically locks itself.
+     * 
+     **/
+
+    public void commit();
+
+    /**
+     *  Returns a {@link Collection} of {@link org.apache.tapestry.record.IPageChange} objects that represent
+     *  the persistant state of the page.
+     *
+     **/
+
+    public Collection getChanges();
+
+    /**
+     *  Returns true if the recorder has any changes for the page.
+     *
+     **/
+
+    public boolean getHasChanges();
+
+    /**
+     *  Returns true if the recorder has observed any changes that have not
+     *  been committed to external storage.
+     *
+     **/
+
+    public boolean isDirty();
+
+    /**
+     *  Returns true if the recorder is in a locked state, following
+     *  a {@link #commit()}.
+     *
+     **/
+
+    public boolean isLocked();
+
+    /**
+     *  Rolls back the page to the currently persisted state.
+     *
+     *  <p>A page recorder can only rollback changes to properties
+     *  which have changed at some point.  This can cause some minor
+     *  problems, addressed by
+     *  {@link org.apache.tapestry.event.PageDetachListener#pageDetached(org.apache.tapestry.event.PageEvent)}.
+     * 
+     **/
+
+    public void rollback(IPage page);
+
+    /**
+     *  Invoked to lock or unlock the recorder.  Recoders are locked
+     *  after they are commited, and stay locked until
+     *  explicitly unlocked in a subsequent request cycle.
+     *
+     **/
+
+    public void setLocked(boolean value);
+
+    /**
+     *  Invoked to mark the recorder for discarding at the end of the request cycle.
+     * 
+     *  @since 2.0.2
+     * 
+     **/
+
+    public void markForDiscard();
+
+    /**
+     *  Returns true if the recorder has been marked for discard.
+     * 
+     **/
+
+    public boolean isMarkedForDiscard();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/IPageSource.java b/tapestry-framework/src/org/apache/tapestry/engine/IPageSource.java
new file mode 100644
index 0000000..c33c98d
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/IPageSource.java
@@ -0,0 +1,75 @@
+//  Copyright 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.tapestry.engine;
+
+import org.apache.tapestry.IPage;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.IResourceResolver;
+
+/**
+ *  Abstracts the process of loading pages from thier specifications as
+ *  well as pooling of pages once loaded.  
+ *
+ *  <p>If the required page is not available, a page source may use an
+ *  instance of {@link IPageLoader} to actually load the
+ *  page (and all of its nested components).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public interface IPageSource
+{
+    /**
+     *  Gets a given page for the engine.  This may involve using a previously
+     *  loaded page from a pool of available pages, or the page may be loaded as needed.
+     * 
+     *  @param cycle the current request cycle
+     *  @param pageName the name of the page.  May be qualified with a library id prefix, which
+     *  may even be nested. Unqualified names are searched for extensively in the application
+     *  namespace, and then in the framework namespace.
+     *  @param monitor informed of any page loading activity
+     *
+     **/
+
+    public IPage getPage(IRequestCycle cycle, String pageName, IMonitor monitor);
+
+    /**
+     *  Invoked after the engine is done with the page
+     *  (typically, after the response to the client has been sent).
+     *  The page is returned to the pool for later reuse.
+     *
+     **/
+
+    public void releasePage(IPage page);
+
+    /**
+     *  Invoked to have the source clear any internal cache.  This is most often
+     *  used when debugging an application.
+     *
+     **/
+
+    public void reset();
+
+    /**
+     * 
+     *  @since 3.0
+     * 
+     **/
+    
+    public IResourceResolver getResourceResolver();
+        
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/IPropertySource.java b/tapestry-framework/src/org/apache/tapestry/engine/IPropertySource.java
new file mode 100644
index 0000000..3361e31
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/IPropertySource.java
@@ -0,0 +1,38 @@
+//  Copyright 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.tapestry.engine;
+
+/**
+ *  A source for configuration properties.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.3
+ *
+ **/
+
+public interface IPropertySource
+{
+    /**
+     *  Returns the value for a given property, or null if the
+     *  source does not provide a value for the named property.
+     *  Implementations of IPropertySource may use delegation
+     *  to resolve the value (that is, if one property source returns null,
+     *  it may forward the request to another source).
+     * 
+     **/
+    
+    public String getPropertyValue(String propertyName);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/IScriptSource.java b/tapestry-framework/src/org/apache/tapestry/engine/IScriptSource.java
new file mode 100644
index 0000000..f9b0ca0
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/IScriptSource.java
@@ -0,0 +1,44 @@
+//  Copyright 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.tapestry.engine;
+
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.IScript;
+
+/**
+ *  Provides access to an {@link IScript}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.2
+ **/
+
+public interface IScriptSource
+{
+    /**
+     *  Retrieves the script identified by the location from the source's
+     *  cache, reading and parsing the script if necessary.
+     * 
+     **/
+
+    public IScript getScript(IResourceLocation scriptLocation);
+
+    /**
+     *  Invoked to clear any cached scripts.
+     *
+     **/
+
+    public void reset();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/ISpecificationSource.java b/tapestry-framework/src/org/apache/tapestry/engine/ISpecificationSource.java
new file mode 100644
index 0000000..c5d7268
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/ISpecificationSource.java
@@ -0,0 +1,104 @@
+//  Copyright 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.tapestry.engine;
+
+import org.apache.tapestry.INamespace;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.spec.IComponentSpecification;
+import org.apache.tapestry.spec.ILibrarySpecification;
+
+/**
+ *  Defines access to component specifications.
+ *
+ *  @see IComponentSpecification
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public interface ISpecificationSource
+{
+    /**
+     *  Retrieves a component specification, parsing it as necessary.
+     *  
+     *  @param specificationLocation the location where the specification
+     *  may be read from.
+     * 
+     *  @throws org.apache.tapestry.ApplicationRuntimeException if the specification doesn't
+     *  exist, is unreadable or invalid.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public IComponentSpecification getComponentSpecification(IResourceLocation specificationLocation);
+
+    /**
+     *  Retrieves a component specification, parsing it as necessary.
+     *  
+     *  @param specificationLocation the location where the specification
+     *  may be read from.
+     * 
+     *  @throws org.apache.tapestry.ApplicationRuntimeException if the specification doesn't
+     *  exist, is unreadable or invalid.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public IComponentSpecification getPageSpecification(IResourceLocation specificationLocation);
+
+    /**
+     *  Invoked to have the source clear any internal cache.  This is most often
+     *  used when debugging an application.
+     *
+     **/
+
+    public void reset();
+
+    /**
+     *  Returns a {@link org.apache.tapestry.spec.LibrarySpecification} with the given path.
+     * 
+     *  @param specificationLocation the resource path of the specification
+     *  to return
+     *  @throws org.apache.tapestry.ApplicationRuntimeException if the specification
+     *  cannot be read
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public ILibrarySpecification getLibrarySpecification(IResourceLocation specificationLocation);
+
+    /**
+     *  Returns the {@link INamespace} for the application.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public INamespace getApplicationNamespace();
+
+    /**
+     *  Returns the {@link INamespace} for the framework itself.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public INamespace getFrameworkNamespace();
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/ITemplateSource.java b/tapestry-framework/src/org/apache/tapestry/engine/ITemplateSource.java
new file mode 100644
index 0000000..febdbcc
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/ITemplateSource.java
@@ -0,0 +1,81 @@
+//  Copyright 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.tapestry.engine;
+
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.parse.ComponentTemplate;
+
+/**
+ * A source of localized HTML templates for components.  
+ * The cache is the means of access for components to load thier templates,
+ * which they need not do until just before rendering.
+ *
+ * <p>The template cache must be able to locate and parse templates as needed.
+ * It may maintain templates in memory.
+ *
+ * @author Howard Ship
+ * @version $Id$
+ * 
+ **/
+
+public interface ITemplateSource
+{
+    /**
+     *  Name of an {@link org.apache.tapestry.IAsset} of a component that provides the template
+     *  for the asset.  This overrides the default (that the template is in
+     *  the same directory as the specification).  This allows
+     *  pages or component templates to be located properly, relative to static
+     *  assets (such as images and stylesheets).
+     * 
+     *  @since 2.2
+     * 
+     **/
+    
+    public static final String TEMPLATE_ASSET_NAME = "$template";
+
+    /**
+     *  Name of the component parameter that will be automatically bound to
+     *  the HTML tag that is used to insert the component in the parent template.
+     *  If the parent component does not have a template (i.e. it extends 
+     *  AbstractComponent, not BaseComponent), then this parameter is bound to null.
+     * 
+     *  @since 3.0
+     * 
+     **/
+    
+    public static final String TEMPLATE_TAG_PARAMETER_NAME = "templateTag";
+    
+    /**
+     *  Locates the template for the component.
+     * 
+     *  @param cycle The request cycle loading the template; this is required
+     *  in some cases when the template is loaded from an {@link org.apache.tapestry.IAsset}.
+     *  @param component The component for which a template should be loaded.
+     *
+     *  @throws org.apache.tapestry.ApplicationRuntimeException if the resource cannot be located or loaded.
+     * 
+     **/
+
+    public ComponentTemplate getTemplate(IRequestCycle cycle, IComponent component);
+
+    /**
+     *  Invoked to have the source clear any internal cache.  This is most often
+     *  used when debugging an application.
+     *
+     **/
+
+    public void reset();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/ITemplateSourceDelegate.java b/tapestry-framework/src/org/apache/tapestry/engine/ITemplateSourceDelegate.java
new file mode 100644
index 0000000..b668296
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/ITemplateSourceDelegate.java
@@ -0,0 +1,54 @@
+//  Copyright 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.tapestry.engine;
+
+import java.util.Locale;
+
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.parse.ComponentTemplate;
+
+/**
+ *  Acts as a delegate to the {@link ITemplateSource}, providing access to
+ *  page and component templates after the normal search mechanisms have failed.
+ * 
+ *  <p>
+ *  The delegate must be threadsafe.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *  @see org.apache.tapestry.engine.DefaultTemplateSource
+ * 
+ **/
+
+public interface ITemplateSourceDelegate
+{
+	/**
+	 *  Invoked by the {@link ITemplateSource} when a template can't be found
+	 *  by normal means (i.e., in the normal locations).  This method
+	 *  should find the template.  The result may be null.  The delegate
+	 *  is responsible for caching the result.
+	 * 
+	 *  @param cycle for access to Tapestry and Servlet API objects
+	 *  @param component component (or page) for which a template is needed
+	 *  @param locale the desired locale for the template
+	 * 
+	 **/
+	
+	public ComponentTemplate findTemplate(IRequestCycle cycle,
+	IComponent component,
+	Locale locale);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/Namespace.java b/tapestry-framework/src/org/apache/tapestry/engine/Namespace.java
new file mode 100644
index 0000000..f94fff1
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/Namespace.java
@@ -0,0 +1,395 @@
+//  Copyright 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.tapestry.engine;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.INamespace;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.resource.ClasspathResourceLocation;
+import org.apache.tapestry.spec.IComponentSpecification;
+import org.apache.tapestry.spec.ILibrarySpecification;
+
+/**
+ *  Implementation of {@link org.apache.tapestry.INamespace}
+ *  that works with a {@link ISpecificationSource} to
+ *  obtain page and component specifications as needed.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.2
+ *
+ **/
+
+public class Namespace implements INamespace
+{
+    private ILibrarySpecification _specification;
+    private ISpecificationSource _specificationSource;
+    private String _id;
+    private String _extendedId;
+    private INamespace _parent;
+    private boolean _frameworkNamespace;
+    private boolean _applicationNamespace;
+
+    /**
+     *  Map of {@link org.apache.tapestry.spec.ComponentSpecification} keyed on page name.
+     *  The map is synchronized because different threads may
+     *  try to update it simultaneously (due to dynamic page
+     *  discovery in the application namespace).
+     * 
+     **/
+
+    private Map _pages = Collections.synchronizedMap(new HashMap());
+
+    /**
+     *  Map of {@link org.apache.tapestry.spec.ComponentSpecification} keyed on
+     *  component alias.
+     * 
+     **/
+
+    private Map _components = Collections.synchronizedMap(new HashMap());
+
+    /**
+     *  Map, keyed on id, of {@link INamespace}.
+     * 
+     **/
+
+    private Map _children = Collections.synchronizedMap(new HashMap());
+
+    public Namespace(
+        String id,
+        INamespace parent,
+        ILibrarySpecification specification,
+        ISpecificationSource specificationSource)
+    {
+        _id = id;
+        _parent = parent;
+        _specification = specification;
+        _specificationSource = specificationSource;
+
+        _applicationNamespace = (_id == null);
+        _frameworkNamespace = FRAMEWORK_NAMESPACE.equals(_id);
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer("Namespace@");
+        buffer.append(Integer.toHexString(hashCode()));
+        buffer.append('[');
+
+        if (_applicationNamespace)
+            buffer.append("<application>");
+        else
+            buffer.append(getExtendedId());
+
+        buffer.append(']');
+
+        return buffer.toString();
+    }
+
+    public String getId()
+    {
+        return _id;
+    }
+
+    public String getExtendedId()
+    {
+        if (_applicationNamespace)
+            return null;
+
+        if (_extendedId == null)
+            _extendedId = buildExtendedId();
+
+        return _extendedId;
+    }
+
+    public INamespace getParentNamespace()
+    {
+        return _parent;
+    }
+
+    public INamespace getChildNamespace(String id)
+    {
+        String firstId = id;
+        String nextIds = null;
+
+        // Split the id into first and next if it is a dot separated sequence
+        int index = id.indexOf('.');
+        if (index >= 0)
+        {
+            firstId = id.substring(0, index);
+            nextIds = id.substring(index + 1);
+        }
+
+        // Get the first namespace
+        INamespace result = (INamespace) _children.get(firstId);
+
+        if (result == null)
+        {
+            result = createNamespace(firstId);
+
+            _children.put(firstId, result);
+        }
+
+        // If the id is a dot separated sequence, recurse to find 
+        // the needed namespace
+        if (result != null && nextIds != null)
+            result = result.getChildNamespace(nextIds);
+
+        return result;
+    }
+
+    public List getChildIds()
+    {
+        return _specification.getLibraryIds();
+    }
+
+    public IComponentSpecification getPageSpecification(String name)
+    {
+        IComponentSpecification result = (IComponentSpecification) _pages.get(name);
+
+        if (result == null)
+        {
+            result = locatePageSpecification(name);
+
+            _pages.put(name, result);
+        }
+
+        return result;
+    }
+
+    public List getPageNames()
+    {
+        Set names = new HashSet();
+
+        names.addAll(_pages.keySet());
+        names.addAll(_specification.getPageNames());
+
+        List result = new ArrayList(names);
+
+        Collections.sort(result);
+
+        return result;
+    }
+
+    public IComponentSpecification getComponentSpecification(String alias)
+    {
+        IComponentSpecification result = (IComponentSpecification) _components.get(alias);
+
+        if (result == null)
+        {
+            result = locateComponentSpecification(alias);
+            _components.put(alias, result);
+        }
+
+        return result;
+    }
+
+    public String getServiceClassName(String name)
+    {
+        return _specification.getServiceClassName(name);
+    }
+
+    public List getServiceNames()
+    {
+        return _specification.getServiceNames();
+    }
+
+    public ILibrarySpecification getSpecification()
+    {
+        return _specification;
+    }
+
+    private String buildExtendedId()
+    {
+        if (_parent == null)
+            return _id;
+
+        String parentId = _parent.getExtendedId();
+
+        // If immediate child of application namespace
+
+        if (parentId == null)
+            return _id;
+
+        return parentId + "." + _id;
+    }
+
+    /**
+     *  Returns a string identifying the namespace, for use in
+     *  error messages.  I.e., "Application namespace" or "namespace 'foo'".
+     * 
+     **/
+
+    public String getNamespaceId()
+    {
+        if (_frameworkNamespace)
+            return Tapestry.getMessage("Namespace.framework-namespace");
+
+        if (_applicationNamespace)
+            return Tapestry.getMessage("Namespace.application-namespace");
+
+        return Tapestry.format("Namespace.nested-namespace", getExtendedId());
+    }
+
+    /**
+     *  Gets the specification from the specification source.
+     * 
+     *  @throws ApplicationRuntimeException if the named page is not defined.
+     * 
+     **/
+
+    private IComponentSpecification locatePageSpecification(String name)
+    {
+        String path = _specification.getPageSpecificationPath(name);
+
+        if (path == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.format("Namespace.no-such-page", name, getNamespaceId()));
+
+        IResourceLocation location = getSpecificationLocation().getRelativeLocation(path);
+
+        return _specificationSource.getPageSpecification(location);
+    }
+
+    private IComponentSpecification locateComponentSpecification(String type)
+    {
+        String path = _specification.getComponentSpecificationPath(type);
+
+        if (path == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.format("Namespace.no-such-alias", type, getNamespaceId()));
+
+        IResourceLocation location = getSpecificationLocation().getRelativeLocation(path);
+
+        return _specificationSource.getComponentSpecification(location);
+    }
+
+    private INamespace createNamespace(String id)
+    {
+        String path = _specification.getLibrarySpecificationPath(id);
+
+        if (path == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.format("Namespace.library-id-not-found", id, getNamespaceId()));
+
+        IResourceLocation location = getSpecificationLocation().getRelativeLocation(path);
+
+        // Ok, an absolute path to a library for an application whose specification
+        // is in the context root is problematic, cause getRelativeLocation()
+        // will still be looking in the context.  Handle this case with the
+        // following little kludge:
+
+        if (location.getResourceURL() == null && path.startsWith("/"))
+            location = new ClasspathResourceLocation(_specification.getResourceResolver(), path);
+
+        ILibrarySpecification ls = _specificationSource.getLibrarySpecification(location);
+
+        return new Namespace(id, this, ls, _specificationSource);
+    }
+
+    public boolean containsPage(String name)
+    {
+        return _pages.containsKey(name) || (_specification.getPageSpecificationPath(name) != null);
+    }
+
+    /** @since 2.3 **/
+
+    public String constructQualifiedName(String pageName)
+    {
+        String prefix = getExtendedId();
+
+        if (prefix == null)
+            return pageName;
+
+        return prefix + SEPARATOR + pageName;
+    }
+
+    /** @since 3.0 **/
+
+    public IResourceLocation getSpecificationLocation()
+    {
+        return _specification.getSpecificationLocation();
+    }
+
+    /** @since 3.0 **/
+
+    public boolean isApplicationNamespace()
+    {
+        return _applicationNamespace;
+    }
+
+    /** @since 3.0 **/
+
+    public synchronized void installPageSpecification(
+        String pageName,
+        IComponentSpecification specification)
+    {
+        _pages.put(pageName, specification);
+    }
+
+    /** @since 3.0 **/
+
+    public synchronized void installComponentSpecification(
+        String type,
+        IComponentSpecification specification)
+    {
+        _components.put(type, specification);
+    }
+
+    /** @since 3.0 **/
+
+    public boolean containsComponentType(String type)
+    {
+        return _components.containsKey(type)
+            || (_specification.getComponentSpecificationPath(type) != null);
+    }
+
+    /** @since 3.0 **/
+
+    public List getComponentTypes()
+    {
+        Set types = new HashSet();
+
+        types.addAll(_components.keySet());
+        types.addAll(_specification.getComponentTypes());
+
+        List result = new ArrayList(types);
+
+        Collections.sort(result);
+
+        return result;
+    }
+
+    /** @since 3.0 **/
+
+    public ILocation getLocation()
+    {
+        if (_specification == null)
+            return null;
+
+        return _specification.getLocation();
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/NullMonitor.java b/tapestry-framework/src/org/apache/tapestry/engine/NullMonitor.java
new file mode 100644
index 0000000..d3a2b6c
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/NullMonitor.java
@@ -0,0 +1,81 @@
+//  Copyright 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.tapestry.engine;
+
+
+
+/**
+ *  Null implementation of {@link org.apache.tapestry.engine.IMonitor}.
+ * 
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class NullMonitor implements IMonitor
+{
+    public static final NullMonitor SHARED = new NullMonitor();
+
+    public void pageCreateBegin(String pageName)
+    {
+    }
+
+    public void pageCreateEnd(String pageName)
+    {
+    }
+
+    public void pageLoadBegin(String pageName)
+    {
+    }
+
+    public void pageLoadEnd(String pageName)
+    {
+    }
+
+    public void pageRenderBegin(String pageName)
+    {
+    }
+
+    public void pageRenderEnd(String pageName)
+    {
+    }
+
+    public void pageRewindBegin(String pageName)
+    {
+    }
+
+    public void pageRewindEnd(String pageName)
+    {
+    }
+
+    public void serviceBegin(String serviceName, String detailMessage)
+    {
+    }
+
+    public void serviceEnd(String serviceName)
+    {
+    }
+
+    public void serviceException(Throwable exception)
+    {
+    }
+
+    public void sessionBegin()
+    {
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/NullTemplateSourceDelegate.java b/tapestry-framework/src/org/apache/tapestry/engine/NullTemplateSourceDelegate.java
new file mode 100644
index 0000000..daa3105
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/NullTemplateSourceDelegate.java
@@ -0,0 +1,59 @@
+//  Copyright 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.tapestry.engine;
+
+import java.util.Locale;
+
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.parse.ComponentTemplate;
+
+/**
+ *  Null implementation of {@link org.apache.tapestry.engine.ITemplateSourceDelegate}. 
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class NullTemplateSourceDelegate implements ITemplateSourceDelegate
+{
+	private static NullTemplateSourceDelegate _shared;
+	
+	/**
+	 *  Returns a shared instance of NullTemplateSourceDelegate.
+	 * 
+	 **/
+	
+	public static NullTemplateSourceDelegate getSharedInstance()
+	{
+		if (_shared == null)
+			_shared = new NullTemplateSourceDelegate();
+			
+		return _shared;
+	}
+
+	/**
+	 *  Simply returns null.
+	 * 
+	 **/
+	
+    public ComponentTemplate findTemplate(IRequestCycle cycle, IComponent component, Locale locale)
+    {
+        return null;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/NullWriter.java b/tapestry-framework/src/org/apache/tapestry/engine/NullWriter.java
new file mode 100644
index 0000000..cfb5d8c
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/NullWriter.java
@@ -0,0 +1,155 @@
+//  Copyright 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.tapestry.engine;
+
+import org.apache.tapestry.IMarkupWriter;
+
+/**
+ *  A {@link IMarkupWriter} that does absolutely <em>nothing</em>; this
+ *  is used during the rewind phase of the request cycle when output
+ *  is discarded anyway.
+ *
+ *  @author Howard Lewis Ship, David Solis
+ *  @version $Id$
+ *  @since 0.2.9
+ *
+ **/
+
+public class NullWriter implements IMarkupWriter
+{
+    private static IMarkupWriter shared;
+
+    public static IMarkupWriter getSharedInstance()
+    {
+        if (shared == null)
+            shared = new NullWriter();
+
+        return shared;
+    }
+
+    public void printRaw(char[] buffer, int offset, int length)
+    {
+    }
+
+    public void printRaw(String value)
+    {
+    }
+
+    public void println()
+    {
+    }
+
+    public void print(char[] data, int offset, int length)
+    {
+    }
+
+    public void print(char value)
+    {
+    }
+
+    public void print(int value)
+    {
+    }
+
+    public void print(String value)
+    {
+    }
+
+    /**
+     *  Returns <code>this</code>: since a NullWriter doesn't actually
+     *  do anything, one is as good as another!.
+     *
+     **/
+
+    public IMarkupWriter getNestedWriter()
+    {
+        return this;
+    }
+
+    public String getContentType()
+    {
+        return null;
+    }
+
+    public void flush()
+    {
+    }
+
+    public void end()
+    {
+    }
+
+    public void end(String name)
+    {
+    }
+
+    public void comment(String value)
+    {
+    }
+
+    public void closeTag()
+    {
+    }
+
+    public void close()
+    {
+    }
+
+    /**
+     *  Always returns false.
+     *
+     **/
+
+    public boolean checkError()
+    {
+        return false;
+    }
+
+    public void beginEmpty(String name)
+    {
+    }
+
+    public void begin(String name)
+    {
+    }
+
+    public void attribute(String name, int value)
+    {
+    }
+
+    public void attribute(String name, String value)
+    {
+    }
+
+    /**
+     *  @see org.apache.tapestry.IMarkupWriter#attribute(java.lang.String, boolean)
+     *
+     *  @since 3.0
+     **/
+
+    public void attribute(String name, boolean value)
+    {
+    }
+
+    /**
+     *  @see org.apache.tapestry.IMarkupWriter#attributeRaw(java.lang.String, java.lang.String)
+     *
+     *  @since 3.0
+     **/
+
+    public void attributeRaw(String name, String value)
+    {
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/PageService.java b/tapestry-framework/src/org/apache/tapestry/engine/PageService.java
new file mode 100644
index 0000000..5467408
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/PageService.java
@@ -0,0 +1,85 @@
+//  Copyright 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.tapestry.engine;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IPage;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.request.RequestContext;
+import org.apache.tapestry.request.ResponseOutputStream;
+
+/**
+ *  Basic server for creating a link to another page in the application.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.9
+ *
+ **/
+
+public class PageService extends AbstractService
+{
+
+    public ILink getLink(IRequestCycle cycle, IComponent component, Object[] parameters)
+    {
+        if (Tapestry.size(parameters) != 1)
+            throw new IllegalArgumentException(
+                Tapestry.format("service-single-parameter", Tapestry.PAGE_SERVICE));
+
+        return constructLink(cycle, Tapestry.PAGE_SERVICE, new String[] {(String) parameters[0]}, null, true);
+
+    }
+
+    public void service(
+        IEngineServiceView engine,
+        IRequestCycle cycle,
+        ResponseOutputStream output)
+        throws ServletException, IOException
+    {
+        RequestContext context = cycle.getRequestContext();
+        String[] serviceContext = getServiceContext(context);
+
+        if (Tapestry.size(serviceContext) != 1)
+            throw new ApplicationRuntimeException(
+                Tapestry.format("service-single-parameter", Tapestry.PAGE_SERVICE));
+
+        String pageName = serviceContext[0];
+
+        // At one time, the page service required a session, but that is no longer necessary.
+        // Users can now bookmark pages within a Tapestry application.  Pages
+        // can implement validate() and throw a PageRedirectException if they don't
+        // want to be accessed this way.  For example, most applications have a concept
+        // of a "login" and have a few pages that don't require the user to be logged in,
+        // and other pages that do.  The protected pages should redirect to a login page.
+
+        IPage page = cycle.getPage(pageName);
+
+        cycle.activate(page);
+
+        engine.renderResponse(cycle, output);
+    }
+
+    public String getName()
+    {
+        return Tapestry.PAGE_SERVICE;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/RequestCycle.java b/tapestry-framework/src/org/apache/tapestry/engine/RequestCycle.java
new file mode 100644
index 0000000..5b57110
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/RequestCycle.java
@@ -0,0 +1,718 @@
+//  Copyright 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.tapestry.engine;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IPage;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.RenderRewoundException;
+import org.apache.tapestry.StaleLinkException;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.event.ChangeObserver;
+import org.apache.tapestry.event.ObservedChangeEvent;
+import org.apache.tapestry.request.RequestContext;
+
+/**
+ *  Provides the logic for processing a single request cycle.  Provides access to
+ *  the {@link IEngine engine} and the {@link RequestContext}.
+ *
+ *  @author Howard Lewis Ship
+ * 
+ **/
+
+public class RequestCycle implements IRequestCycle, ChangeObserver
+{
+    private static final Log LOG = LogFactory.getLog(RequestCycle.class);
+
+    private IPage _page;
+    private IEngine _engine;
+    private IEngineService _service;
+
+    private RequestContext _requestContext;
+
+    private IMonitor _monitor;
+
+    private HttpServletResponse _response;
+
+    /**
+     *  A mapping of pages loaded during the current request cycle.
+     *  Key is the page name, value is the {@link IPage} instance.
+     *
+     **/
+
+    private Map _loadedPages;
+
+    /**
+     * A mapping of page recorders for the current request cycle.
+     * Key is the page name, value is the {@link IPageRecorder} instance.
+     *
+     **/
+
+    private Map _loadedRecorders;
+
+    private boolean _rewinding = false;
+
+    private Map _attributes;
+
+    private int _actionId;
+    private int _targetActionId;
+    private IComponent _targetComponent;
+
+    /** @since 2.0.3 **/
+
+    private Object[] _serviceParameters;
+
+    /**
+     *  Standard constructor used to render a response page.
+     *
+     **/
+
+    public RequestCycle(
+        IEngine engine,
+        RequestContext requestContext,
+        IEngineService service,
+        IMonitor monitor)
+    {
+        _engine = engine;
+        _requestContext = requestContext;
+        _service = service;
+        _monitor = monitor;
+    }
+
+    /**
+     *  Called at the end of the request cycle (i.e., after all responses have been
+     *  sent back to the client), to release all pages loaded during the request cycle.
+     *
+     **/
+
+    public void cleanup()
+    {
+        if (_loadedPages == null)
+            return;
+
+        IPageSource source = _engine.getPageSource();
+        Iterator i = _loadedPages.values().iterator();
+
+        while (i.hasNext())
+        {
+            IPage page = (IPage) i.next();
+
+            source.releasePage(page);
+        }
+
+        _loadedPages = null;
+        _loadedRecorders = null;
+
+    }
+
+    public IEngineService getService()
+    {
+        return _service;
+    }
+
+    public String encodeURL(String URL)
+    {
+        if (_response == null)
+            _response = _requestContext.getResponse();
+
+        return _response.encodeURL(URL);
+    }
+
+    public IEngine getEngine()
+    {
+        return _engine;
+    }
+
+    public Object getAttribute(String name)
+    {
+        if (_attributes == null)
+            return null;
+
+        return _attributes.get(name);
+    }
+
+    public IMonitor getMonitor()
+    {
+        return _monitor;
+    }
+
+    public String getNextActionId()
+    {
+        return Integer.toHexString(++_actionId);
+    }
+
+    public IPage getPage()
+    {
+        return _page;
+    }
+
+    /**
+     *  Gets the page from the engines's {@link IPageSource}.
+     *
+     **/
+
+    public IPage getPage(String name)
+    {
+        IPage result = null;
+
+        if (name == null)
+            throw new NullPointerException(Tapestry.getMessage("RequestCycle.invalid-null-name"));
+
+        if (_loadedPages != null)
+            result = (IPage) _loadedPages.get(name);
+
+        if (result == null)
+        {
+            _monitor.pageLoadBegin(name);
+
+            IPageSource pageSource = _engine.getPageSource();
+
+            result = pageSource.getPage(this, name, _monitor);
+
+            // Get the recorder that will eventually observe and record
+            // changes to persistent properties of the page.  If the page
+            // has never emitted any page changes, then it will
+            // not have a recorder.
+
+            IPageRecorder recorder = getPageRecorder(name);
+
+            if (recorder != null)
+            {
+                // Have it rollback the page to the prior state.  Note that
+                // the page has a null observer at this time.
+
+                recorder.rollback(result);
+
+                // Now, have the page use the recorder for any future
+                // property changes.
+
+                result.setChangeObserver(recorder);
+
+                // And, if this recorder observed changes in a prior request cycle
+                // (and was locked after committing in that cycle), it's time
+                // to unlock.
+
+                recorder.setLocked(false);
+            }
+            else
+            {
+                // No page recorder for the page.  We'll observe its
+                // changes and create the page recorder dynamically
+                // if it emits any.
+
+                result.setChangeObserver(this);
+            }
+
+            _monitor.pageLoadEnd(name);
+
+            if (_loadedPages == null)
+                _loadedPages = new HashMap();
+
+            _loadedPages.put(name, result);
+        }
+
+        return result;
+    }
+
+    /**
+     *  Returns the page recorder for the named page.  This may come
+     *  from the cycle's cache of page recorders or, if not yet encountered
+     *  in this request cycle, the {@link IEngine#getPageRecorder(String, IRequestCycle)} is
+     *  invoked to get the recorder, if it exists.
+     * 
+     **/
+
+    protected IPageRecorder getPageRecorder(String name)
+    {
+        IPageRecorder result = null;
+
+        if (_loadedRecorders != null)
+            result = (IPageRecorder) _loadedRecorders.get(name);
+
+        if (result != null)
+            return result;
+
+        result = _engine.getPageRecorder(name, this);
+
+        if (result == null)
+            return null;
+
+        if (_loadedRecorders == null)
+            _loadedRecorders = new HashMap();
+
+        _loadedRecorders.put(name, result);
+
+        return result;
+    }
+
+    /** 
+     * 
+     *  Gets the page recorder from the loadedRecorders cache, or from the engine
+     *  (putting it into loadedRecorders).  If the recorder does not yet exist,
+     *  it is created.
+     * 
+     *  @see IEngine#createPageRecorder(String, IRequestCycle)
+     *  @since 2.0.3
+     * 
+     **/
+
+    private IPageRecorder createPageRecorder(String name)
+    {
+        IPageRecorder result = getPageRecorder(name);
+
+        if (result == null)
+        {
+            result = _engine.createPageRecorder(name, this);
+
+            if (_loadedRecorders == null)
+                _loadedRecorders = new HashMap();
+
+            _loadedRecorders.put(name, result);
+        }
+
+        return result;
+    }
+
+    public RequestContext getRequestContext()
+    {
+        return _requestContext;
+    }
+
+    public boolean isRewinding()
+    {
+        return _rewinding;
+    }
+
+    public boolean isRewound(IComponent component) throws StaleLinkException
+    {
+        // If not rewinding ...
+
+        if (!_rewinding)
+            return false;
+
+        if (_actionId != _targetActionId)
+            return false;
+
+        // OK, we're there, is the page is good order?
+
+        if (component == _targetComponent)
+            return true;
+
+        // Woops.  Mismatch.
+
+        throw new StaleLinkException(
+            component,
+            Integer.toHexString(_targetActionId),
+            _targetComponent.getExtendedId());
+    }
+
+    public void removeAttribute(String name)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Removing attribute " + name);
+
+        if (_attributes == null)
+            return;
+
+        _attributes.remove(name);
+    }
+
+    /**
+     *  Renders the page by invoking 
+     * {@link IPage#renderPage(IMarkupWriter, IRequestCycle)}.  
+     *  This clears all attributes.
+     *
+     **/
+
+    public void renderPage(IMarkupWriter writer)
+    {
+        String pageName = _page.getPageName();
+        _monitor.pageRenderBegin(pageName);
+
+        _rewinding = false;
+        _actionId = -1;
+        _targetActionId = 0;
+
+        // Forget any attributes from a previous render cycle.
+
+        if (_attributes != null)
+            _attributes.clear();
+
+        try
+        {
+            _page.renderPage(writer, this);
+
+        }
+        catch (ApplicationRuntimeException ex)
+        {
+            // Nothing much to add here.
+
+            throw ex;
+        }
+        catch (Throwable ex)
+        {
+            // But wrap other exceptions in a RequestCycleException ... this
+            // will ensure that some of the context is available.
+
+            throw new ApplicationRuntimeException(ex.getMessage(), _page, null, ex);
+        }
+        finally
+        {
+            _actionId = 0;
+            _targetActionId = 0;
+        }
+
+        _monitor.pageRenderEnd(pageName);
+
+    }
+
+    /**
+     *  Rewinds an individual form by invoking 
+     *  {@link IForm#rewind(IMarkupWriter, IRequestCycle)}.
+     *
+     * <p>The process is expected to end with a {@link RenderRewoundException}.
+     * If the entire page is renderred without this exception being thrown, it means
+     * that the target action id was not valid, and a 
+     * {@link ApplicationRuntimeException}
+     * is thrown.
+     *
+     * <p>This clears all attributes.
+     *
+     *  @since 1.0.2
+     **/
+
+    public void rewindForm(IForm form, String targetActionId)
+    {
+        IPage page = form.getPage();
+        String pageName = page.getPageName();
+
+        _rewinding = true;
+
+        _monitor.pageRewindBegin(pageName);
+
+        if (_attributes != null)
+            _attributes.clear();
+
+        // Fake things a little for getNextActionId() / isRewound()
+
+        _targetActionId = Integer.parseInt(targetActionId, 16);
+        _actionId = _targetActionId - 1;
+
+        _targetComponent = form;
+
+        try
+        {
+            page.beginPageRender();
+
+            form.rewind(NullWriter.getSharedInstance(), this);
+
+            // Shouldn't get this far, because the form should
+            // throw the RenderRewoundException.
+
+            throw new StaleLinkException(
+                Tapestry.format("RequestCycle.form-rewind-failure", form.getExtendedId()),
+                form);
+        }
+        catch (RenderRewoundException ex)
+        {
+            // This is acceptible and expected.
+        }
+        catch (ApplicationRuntimeException ex)
+        {
+            // RequestCycleExceptions don't need to be wrapped.
+            throw ex;
+        }
+        catch (Throwable ex)
+        {
+            // But wrap other exceptions in a ApplicationRuntimeException ... this
+            // will ensure that some of the context is available.
+
+            throw new ApplicationRuntimeException(ex.getMessage(), page, null, ex);
+        }
+        finally
+        {
+            _actionId = 0;
+            _targetActionId = 0;
+            _targetComponent = null;
+
+            page.endPageRender();
+
+            _monitor.pageRewindEnd(pageName);
+
+            _rewinding = false;
+        }
+    }
+
+    /**
+     *  Rewinds the page by invoking 
+     *  {@link IPage#renderPage(IMarkupWriter, IRequestCycle)}.
+     *
+     * <p>The process is expected to end with a {@link RenderRewoundException}.
+     * If the entire page is renderred without this exception being thrown, it means
+     * that the target action id was not valid, and a 
+     * {@link ApplicationRuntimeException}
+     * is thrown.
+     *
+     * <p>This clears all attributes.
+     *
+     **/
+
+    public void rewindPage(String targetActionId, IComponent targetComponent)
+    {
+        String pageName = _page.getPageName();
+
+        _rewinding = true;
+
+        _monitor.pageRewindBegin(pageName);
+
+        if (_attributes != null)
+            _attributes.clear();
+
+        _actionId = -1;
+
+        // Parse the action Id as hex since that's whats generated
+        // by getNextActionId()
+        _targetActionId = Integer.parseInt(targetActionId, 16);
+        _targetComponent = targetComponent;
+
+        try
+        {
+            _page.renderPage(NullWriter.getSharedInstance(), this);
+
+            // Shouldn't get this far, because the target component should
+            // throw the RenderRewoundException.
+
+            throw new StaleLinkException(_page, targetActionId, targetComponent.getExtendedId());
+        }
+        catch (RenderRewoundException ex)
+        {
+            // This is acceptible and expected.
+        }
+        catch (ApplicationRuntimeException ex)
+        {
+            // ApplicationRuntimeExceptions don't need to be wrapped.
+            throw ex;
+        }
+        catch (Throwable ex)
+        {
+            // But wrap other exceptions in a RequestCycleException ... this
+            // will ensure that some of the context is available.
+
+            throw new ApplicationRuntimeException(ex.getMessage(), _page, null, ex);
+        }
+        finally
+        {
+
+            _actionId = 0;
+            _targetActionId = 0;
+            _targetComponent = null;
+
+            _monitor.pageRewindEnd(pageName);
+
+            _rewinding = false;
+        }
+
+    }
+
+    public void setAttribute(String name, Object value)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Set attribute " + name + " to " + value);
+
+        if (_attributes == null)
+            _attributes = new HashMap();
+
+        _attributes.put(name, value);
+    }
+
+    public void setPage(IPage value)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Set page to " + value);
+
+        _page = value;
+    }
+
+    public void setPage(String name)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Set page to " + name);
+
+        _page = getPage(name);
+    }
+
+    /**
+     *  Invokes {@link IPageRecorder#commit()} on each page recorder loaded
+     *  during the request cycle (even recorders marked for discard).
+     *
+     **/
+
+    public void commitPageChanges()
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Committing page changes");
+
+        if (_loadedRecorders == null || _loadedRecorders.isEmpty())
+            return;
+
+        Iterator i = _loadedRecorders.values().iterator();
+
+        while (i.hasNext())
+        {
+            IPageRecorder recorder = (IPageRecorder) i.next();
+
+            recorder.commit();
+        }
+    }
+
+    /**
+     *  For pages without a {@link IPageRecorder page recorder}, 
+     *  we're the {@link ChangeObserver change observer}.
+     *  If such a page actually emits a change, then
+     *  we'll obtain a new page recorder from the
+     *  {@link IEngine engine}, set the recorder
+     *  as the page's change observer, and forward the event
+     *  to the newly created recorder.  In addition, the
+     *  new page recorder is remembered so that it will
+     *  be committed by {@link #commitPageChanges()}.
+     *
+     **/
+
+    public void observeChange(ObservedChangeEvent event)
+    {
+        IPage page = event.getComponent().getPage();
+        String pageName = page.getPageName();
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Observed change in page " + pageName + "; creating page recorder.");
+
+        IPageRecorder recorder = createPageRecorder(pageName);
+
+        page.setChangeObserver(recorder);
+
+        recorder.observeChange(event);
+    }
+
+    /**
+     *  Finds the page and its page recorder, creating the page recorder if necessary.
+     *  The page recorder is marked for discard regardless of its current state.
+     * 
+     *  <p>This may make the application stateful even if the page recorder does
+     *  not yet exist.
+     * 
+     *  <p>The page recorder will be discarded at the end of the current request cycle.
+     * 
+     *  @since 2.0.2
+     * 
+     **/
+
+    public void discardPage(String name)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Discarding page " + name);
+
+        IPageRecorder recorder = _engine.getPageRecorder(name, this);
+
+        if (recorder == null)
+        {
+            _page = getPage(name);
+
+            recorder = createPageRecorder(name);
+
+            _page.setChangeObserver(recorder);
+        }
+
+        recorder.markForDiscard();
+    }
+
+    /** @since 2.0.3 **/
+
+    public Object[] getServiceParameters()
+    {
+        return _serviceParameters;
+    }
+
+    /** @since 2.0.3 **/
+
+    public void setServiceParameters(Object[] serviceParameters)
+    {
+        _serviceParameters = serviceParameters;
+    }
+
+    /** @since 3.0 **/
+
+    public void activate(String name)
+    {
+        IPage page = getPage(name);
+
+        activate(page);
+    }
+
+    /** @since 3.0 */
+
+    public void activate(IPage page)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Activating page " + page);
+
+        Tapestry.clearMethodInvocations();
+
+        page.validate(this);
+
+        Tapestry.checkMethodInvocation(
+            Tapestry.ABSTRACTPAGE_VALIDATE_METHOD_ID,
+            "validate()",
+            page);
+
+        setPage(page);
+    }
+
+    /**
+     * @since 3.0
+     */
+    public String toString()
+    {
+        ToStringBuilder b = new ToStringBuilder(this);
+
+        b.append("rewinding", _rewinding);
+
+        if (_service != null)
+            b.append("service", _service.getName());
+
+        b.append("serviceParameters", _serviceParameters);
+
+        if (_loadedPages != null)
+            b.append("loadedPages", _loadedPages.keySet());
+
+        b.append("attributes", _attributes);
+        b.append("targetActionId", _targetActionId);
+        b.append("targetComponent", _targetComponent);
+
+        return b.toString();
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/ResetService.java b/tapestry-framework/src/org/apache/tapestry/engine/ResetService.java
new file mode 100644
index 0000000..f76e955
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/ResetService.java
@@ -0,0 +1,88 @@
+//  Copyright 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.tapestry.engine;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IPage;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.request.ResponseOutputStream;
+
+/**
+ *  ServiceLink used to discard all cached data (templates, specifications, et cetera).
+ *  This is primarily used during development.  It could be a weakness of a Tapestry
+ *  application, making it susceptible to denial of service attacks, which is why
+ *  it is disabled by default.  The link generated by the ResetService redisplays the
+ *  current page after discarding all data.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.9
+ *  @see org.apache.tapestry.IEngine#isResetServiceEnabled()
+ * 
+ **/
+
+public class ResetService extends AbstractService
+{
+
+    public ILink getLink(IRequestCycle cycle, IComponent component, Object[] parameters)
+    {
+        if (Tapestry.size(parameters) != 0)
+            throw new IllegalArgumentException(
+                Tapestry.format("service-no-parameters", Tapestry.RESET_SERVICE));
+
+        String[] context = new String[1];
+        context[0] = component.getPage().getPageName();
+
+        return constructLink(cycle, Tapestry.RESET_SERVICE, context, null, true);
+    }
+
+    public String getName()
+    {
+        return Tapestry.RESET_SERVICE;
+    }
+
+    public void service(
+        IEngineServiceView engine,
+        IRequestCycle cycle,
+        ResponseOutputStream output)
+        throws ServletException, IOException
+    {
+        String[] context = getServiceContext(cycle.getRequestContext());
+
+        if (Tapestry.size(context) != 1)
+            throw new ApplicationRuntimeException(
+                Tapestry.format("service-single-parameter", Tapestry.RESET_SERVICE));
+
+        String pageName = context[0];
+
+        if (engine.isResetServiceEnabled())
+            engine.clearCachedData();
+
+        IPage page = cycle.getPage(pageName);
+
+        cycle.activate(page);
+
+        // Render the same page (that contained the reset link).
+
+        engine.renderResponse(cycle, output);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/RestartService.java b/tapestry-framework/src/org/apache/tapestry/engine/RestartService.java
new file mode 100644
index 0000000..bdb64bc
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/RestartService.java
@@ -0,0 +1,63 @@
+//  Copyright 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.tapestry.engine;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.request.ResponseOutputStream;
+
+/**
+ *  Restarts the Tapestry application.  This is normally reserved for dealing with
+ *  catastrophic failures of the application.  Discards the {@link javax.servlet.http.HttpSession}, if any,
+ *  and redirects to the Tapestry application servlet URL (invoking the {@link HomeService}).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.9
+ *
+ **/
+
+public class RestartService extends AbstractService
+{
+
+    public ILink getLink(IRequestCycle cycle, IComponent component, Object[] parameters)
+    {
+        if (Tapestry.size(parameters) != 0)
+            throw new IllegalArgumentException(
+                Tapestry.format("service-no-parameters", Tapestry.RESTART_SERVICE));
+
+        return constructLink(cycle, Tapestry.RESTART_SERVICE, null, null, true);
+    }
+
+    public void service(
+        IEngineServiceView engine,
+        IRequestCycle cycle,
+        ResponseOutputStream output)
+        throws ServletException, IOException
+    {
+        engine.restart(cycle);
+    }
+
+    public String getName()
+    {
+        return Tapestry.RESTART_SERVICE;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/TagSupportService.java b/tapestry-framework/src/org/apache/tapestry/engine/TagSupportService.java
new file mode 100644
index 0000000..faaedd3
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/TagSupportService.java
@@ -0,0 +1,160 @@
+//  Copyright 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.tapestry.engine;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Enumeration;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.html.HTMLWriter;
+import org.apache.tapestry.request.RequestContext;
+import org.apache.tapestry.request.ResponseOutputStream;
+
+/**
+ *  A very specialized service used by JSPs to access Tapestry URLs. 
+ *  This is used by the Tapestry JSP tags, such as
+ *  {@link org.apache.tapestry.jsp.PageTag}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *  @see org.apache.tapestry.jsp.URLRetriever
+ * 
+ **/
+
+public class TagSupportService implements IEngineService
+{
+    private static final Log LOG = LogFactory.getLog(TagSupportService.class);
+
+    /**
+     *  Not to be invoked; this service is different than the others.
+     * 
+     *  @throws ApplicationRuntimeException always
+     * 
+     **/
+
+    public ILink getLink(IRequestCycle cycle, IComponent component, Object[] parameters)
+    {
+        throw new ApplicationRuntimeException(
+            Tapestry.getMessage("TagSupportService.service-only"));
+    }
+
+    public void service(
+        IEngineServiceView engine,
+        IRequestCycle cycle,
+        ResponseOutputStream output)
+        throws ServletException, IOException
+    {
+        RequestContext context = cycle.getRequestContext();
+        HttpServletRequest request = context.getRequest();
+
+        String serviceName = getAttribute(request, Tapestry.TAG_SUPPORT_SERVICE_ATTRIBUTE);
+
+        Object raw = request.getAttribute(Tapestry.TAG_SUPPORT_PARAMETERS_ATTRIBUTE);
+        Object[] parameters = null;
+
+        try
+        {
+            parameters = (Object[]) raw;
+        }
+        catch (ClassCastException ex)
+        {
+            throw new ServletException(
+                Tapestry.format(
+                    "TagSupportService.attribute-not-array",
+                    Tapestry.TAG_SUPPORT_PARAMETERS_ATTRIBUTE,
+                    Tapestry.getClassName(raw.getClass())));
+        }
+
+        IEngineService service = cycle.getEngine().getService(serviceName);
+
+        ILink link = service.getLink(cycle, null, parameters);
+
+        String URI = link.getURL();
+
+        if (LOG.isDebugEnabled())
+        {
+        	LOG.debug("Request servlet path = " + request.getServletPath());
+        	
+            Enumeration e = request.getParameterNames();
+            while (e.hasMoreElements())
+            {
+                String name = (String) e.nextElement();
+                LOG.debug("Request parameter " + name + " = " + request.getParameter(name));
+            }
+            e = request.getAttributeNames();
+            while (e.hasMoreElements())
+            {
+                String name = (String) e.nextElement();
+                LOG.debug("Request attribute " + name + " = " + request.getAttribute(name));
+            }
+
+            LOG.debug("Result URI: " + URI);
+        }
+
+        HttpServletResponse response = context.getResponse();
+        PrintWriter servletWriter = response.getWriter();
+
+        IMarkupWriter writer = new HTMLWriter(servletWriter);
+
+        writer.print(URI);
+
+        writer.flush();
+    }
+
+    private String getAttribute(HttpServletRequest request, String name) throws ServletException
+    {
+        Object result = request.getAttribute(name);
+
+        if (result == null)
+            throw new ServletException(Tapestry.format("TagSupportService.null-attribute", name));
+
+        try
+        {
+            return (String) result;
+        }
+        catch (ClassCastException ex)
+        {
+            throw new ServletException(
+                Tapestry.format(
+                    "TagSupportService.attribute-not-string",
+                    name,
+                    Tapestry.getClassName(result.getClass())));
+
+        }
+    }
+
+    /**
+     *  @return {@link org.apache.tapestry.Tapestry#TAGSUPPORT_SERVICE}.
+     * 
+     **/
+
+    public String getName()
+    {
+        return Tapestry.TAGSUPPORT_SERVICE;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/TemplateParserDelegateImpl.java b/tapestry-framework/src/org/apache/tapestry/engine/TemplateParserDelegateImpl.java
new file mode 100644
index 0000000..b5cbe1f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/TemplateParserDelegateImpl.java
@@ -0,0 +1,70 @@
+//  Copyright 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.tapestry.engine;
+
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.INamespace;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.parse.ITemplateParserDelegate;
+import org.apache.tapestry.resolver.ComponentSpecificationResolver;
+import org.apache.tapestry.spec.IComponentSpecification;
+
+/**
+ * Basic implementation of the {@link org.apache.tapestry.parse.ITemplateParserDelegate} interface.
+ *
+ * @author Howard Lewis Ship
+ */
+public class TemplateParserDelegateImpl implements ITemplateParserDelegate
+{
+    private IComponent _component;
+    private ComponentSpecificationResolver _resolver;
+    private IRequestCycle _cycle;
+
+    public TemplateParserDelegateImpl(IComponent component, IRequestCycle cycle)
+    {
+        _component = component;
+        _resolver = new ComponentSpecificationResolver(cycle);
+        _cycle = cycle;
+    }
+
+    public boolean getKnownComponent(String componentId)
+    {
+        return _component.getSpecification().getComponent(componentId) != null;
+    }
+
+    public boolean getAllowBody(String componentId, ILocation location)
+    {
+        IComponent embedded = _component.getComponent(componentId);
+
+        if (embedded == null)
+            throw Tapestry.createNoSuchComponentException(_component, componentId, location);
+
+        return embedded.getSpecification().getAllowBody();
+    }
+
+    public boolean getAllowBody(String libraryId, String type, ILocation location)
+    {
+        INamespace namespace = _component.getNamespace();
+
+        _resolver.resolve(_cycle, namespace, libraryId, type, location);
+
+        IComponentSpecification spec = _resolver.getSpecification();
+
+        return spec.getAllowBody();
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/engine/package.html b/tapestry-framework/src/org/apache/tapestry/engine/package.html
new file mode 100644
index 0000000..d6ea383
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/engine/package.html
@@ -0,0 +1,19 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+<p>Implementations of the {@link org.apache.tapestry.IEngine} interface, including
+the standard implementation:
+{@link org.apache.tapestry.engine.BaseEngine}.  Also located here are
+default implementations of all the basic support objects, including
+{@link org.apache.tapestry.engine.RequestCycle} 
+(which implements {@link org.apache.tapestry.IRequestCycle}).
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/enhance/BaseEnhancedClass.java b/tapestry-framework/src/org/apache/tapestry/enhance/BaseEnhancedClass.java
new file mode 100644
index 0000000..49b0bce
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/enhance/BaseEnhancedClass.java
@@ -0,0 +1,74 @@
+//  Copyright 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.tapestry.enhance;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ *  @author Mindbridge
+ *  @version $Id$
+ *  @since 3.0
+ *
+ */
+public abstract class BaseEnhancedClass implements IEnhancedClass
+{
+
+    /**
+     *  List of {@link IEnhancer}.
+     * 
+     **/
+    private List _enhancers;
+
+    protected List getEnhancers()
+    {
+        return _enhancers;
+    }
+
+    public void addEnhancer(IEnhancer enhancer)
+    {
+        if (_enhancers == null)
+            _enhancers = new ArrayList();
+
+        _enhancers.add(enhancer);
+    }
+
+    /**
+     * @see org.apache.tapestry.enhance.IEnhancedClass#hasModifications()
+     */
+    public boolean hasModifications()
+    {
+        return _enhancers != null && !_enhancers.isEmpty();
+    }
+
+    public void performEnhancement()
+    {
+        List enhancers = getEnhancers();
+
+        if (enhancers == null)
+            return;
+
+        int count = enhancers.size();
+
+        for (int i = 0; i < count; i++)
+        {
+            IEnhancer enhancer = (IEnhancer) enhancers.get(i);
+
+            enhancer.performEnhancement(this);
+        }
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/enhance/CodeGenerationException.java b/tapestry-framework/src/org/apache/tapestry/enhance/CodeGenerationException.java
new file mode 100644
index 0000000..a7c91b1
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/enhance/CodeGenerationException.java
@@ -0,0 +1,59 @@
+//  Copyright 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.tapestry.enhance;
+
+/**
+ *  This is an unrecoverable error during code generation.
+ *  It should not occur and would typically be the result 
+ *  of a bug in the Tapestry code.
+ *   
+ *  @author Mindbridge
+ *  @version $Id$
+ *  @since 3.0
+ */
+public class CodeGenerationException extends RuntimeException
+{
+	Throwable _cause;
+
+    public CodeGenerationException()
+    {
+        super();
+    }
+
+    public CodeGenerationException(String message)
+    {
+        super(message);
+    }
+
+    public CodeGenerationException(String message, Throwable cause)
+    {
+        super(message);
+        _cause = cause;
+    }
+
+    public CodeGenerationException(Throwable cause)
+    {
+        super();
+        _cause = cause;
+    }
+    
+    public Throwable getCause()
+    {
+    	return _cause;
+    }
+    
+    
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/enhance/ComponentClassFactory.java b/tapestry-framework/src/org/apache/tapestry/enhance/ComponentClassFactory.java
new file mode 100644
index 0000000..e56bad4
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/enhance/ComponentClassFactory.java
@@ -0,0 +1,529 @@
+//  Copyright 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.tapestry.enhance;
+
+import java.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.spec.Direction;
+import org.apache.tapestry.spec.IComponentSpecification;
+import org.apache.tapestry.spec.IParameterSpecification;
+import org.apache.tapestry.spec.IPropertySpecification;
+
+/**
+ *  Contains the logic for analyzing and enhancing a single component class.
+ *  Internally, this class makes use of {@link IEnhancedClassFactory}.
+ *
+ *  @author Howard Lewis Ship
+ *  @since 3.0
+ *
+ **/
+
+public class ComponentClassFactory
+{
+    private static final Log LOG = LogFactory.getLog(ComponentClassFactory.class);
+
+    /**
+     *  Package prefix to be added if the enhanced object is in a 'sysem' package 
+     */
+    private static final String PACKAGE_PREFIX = "org.apache.tapestry.";
+
+    /**
+     *  UID used to generate new class names.
+     **/
+    private static int _uid = 0;
+
+    /**
+     *  Mapping between a primitive type and its Java VM representation
+     *  Used for the encoding of array types
+     **/
+    private static Map _primitiveTypes = new HashMap();
+
+    static {
+        _primitiveTypes.put("boolean", "Z");
+        _primitiveTypes.put("short", "S");
+        _primitiveTypes.put("int", "I");
+        _primitiveTypes.put("long", "J");
+        _primitiveTypes.put("float", "F");
+        _primitiveTypes.put("double", "D");
+        _primitiveTypes.put("char", "C");
+        _primitiveTypes.put("byte", "B");
+    }
+
+    private IResourceResolver _resolver;
+
+    private IEnhancedClassFactory _enhancedClassFactory;
+    private IEnhancedClass _enhancedClass;
+    private Map _beanProperties = new HashMap();
+    private IComponentSpecification _specification;
+    private Class _componentClass;
+    private JavaClassMapping _classMapping = new JavaClassMapping();
+
+    public ComponentClassFactory(
+        IResourceResolver resolver,
+        IComponentSpecification specification,
+        Class componentClass,
+        IEnhancedClassFactory enhancedClassFactory)
+    {
+        _resolver = resolver;
+
+        _specification = specification;
+
+        _componentClass = componentClass;
+
+        _enhancedClassFactory = enhancedClassFactory;
+
+        buildBeanProperties();
+    }
+
+    private void buildBeanProperties()
+    {
+        BeanInfo info = null;
+
+        try
+        {
+            info = Introspector.getBeanInfo(_componentClass);
+
+        }
+        catch (IntrospectionException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "ComponentClassFactory.unable-to-introspect-class",
+                    _componentClass.getName()),
+                ex);
+        }
+
+        PropertyDescriptor[] descriptors = info.getPropertyDescriptors();
+
+        for (int i = 0; i < descriptors.length; i++)
+        {
+            _beanProperties.put(descriptors[i].getName(), descriptors[i]);
+        }
+    }
+
+    protected PropertyDescriptor getPropertyDescriptor(String name)
+    {
+        return (PropertyDescriptor) _beanProperties.get(name);
+    }
+
+    /**
+     *  Invokes {@link #scanForEnhancements()} to identify any
+     *  enhancements needed on the class, returning true
+     *  if there are any enhancements to be performed. 
+     * 
+     **/
+
+    public boolean needsEnhancement()
+    {
+        scanForEnhancements();
+
+        return _enhancedClass != null && _enhancedClass.hasModifications();
+    }
+
+    /**
+     * @return true if pd is not null and both read/write methods are implemented
+     */
+    public boolean isImplemented(PropertyDescriptor pd)
+    {
+        if (pd == null)
+            return false;
+
+        return isImplemented(pd.getReadMethod()) && isImplemented(pd.getWriteMethod());
+    }
+
+    /**
+     * @return true if m is not null and is abstract.
+     */
+    public boolean isAbstract(Method m)
+    {
+        if (m == null)
+            return false;
+
+        return Modifier.isAbstract(m.getModifiers());
+    }
+
+    /**
+     * @return true if m is not null and not abstract  
+     */
+    public boolean isImplemented(Method m)
+    {
+        if (m == null)
+            return false;
+
+        return !Modifier.isAbstract(m.getModifiers());
+    }
+
+    /**
+     *  Given a class name, returns the corresponding class.  In addition,
+     *  scalar types, arrays of scalar types, java.lang.Object[] and
+     *  java.lang.String[] are supported.
+     * 
+     *  @param type to convert to a Class
+     *  @param location of the involved specification element (for exception reporting)
+     * 
+     **/
+
+    public Class convertPropertyType(String type, ILocation location)
+    {
+        Class result = _classMapping.getType(type);
+
+        if (result == null)
+        {
+            try
+            {
+                String typeName = translateClassName(type);
+                result = _resolver.findClass(typeName);
+            }
+            catch (Exception ex)
+            {
+                throw new ApplicationRuntimeException(
+                    Tapestry.format("ComponentClassFactory.bad-property-type", type),
+                    location,
+                    ex);
+            }
+
+            _classMapping.recordType(type, result);
+        }
+
+        return result;
+    }
+
+    /**
+     *  Translates types from standard Java format to Java VM format.
+     *  For example, java.util.Locale remains java.util.Locale, but
+     *  int[][] is translated to [[I and java.lang.Object[] to 
+     *  [Ljava.lang.Object;   
+     *  This method and its static Map should go into a utility class
+     */
+    protected String translateClassName(String type)
+    {
+        // if it is not an array, just return the type itself
+        if (!type.endsWith("[]"))
+            return type;
+
+        // if it is an array, convert it to JavaVM-style format
+        StringBuffer javaType = new StringBuffer();
+        while (type.endsWith("[]"))
+        {
+            javaType.append("[");
+            type = type.substring(0, type.length() - 2);
+        }
+
+        String primitiveIdentifier = (String) _primitiveTypes.get(type);
+        if (primitiveIdentifier != null)
+            javaType.append(primitiveIdentifier);
+        else
+            javaType.append("L" + type + ";");
+
+        return javaType.toString();
+    }
+
+    protected void checkPropertyType(PropertyDescriptor pd, Class propertyType, ILocation location)
+    {
+        if (!pd.getPropertyType().equals(propertyType))
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "ComponentClassFactory.property-type-mismatch",
+                    new Object[] {
+                        _componentClass.getName(),
+                        pd.getName(),
+                        pd.getPropertyType().getName(),
+                        propertyType.getName()}),
+                location,
+                null);
+    }
+
+    /**
+     *  Checks to see that that class either doesn't provide the property, or does
+     *  but the accessor(s) are abstract.  Returns the name of the read accessor,
+     *  or null if there is no such accessor (this is helpful if the beanClass
+     *  defines a boolean property, where the name of the accessor may be isXXX or
+     *  getXXX).
+     * 
+     **/
+
+    protected String checkAccessors(String propertyName, Class propertyType, ILocation location)
+    {
+        PropertyDescriptor d = getPropertyDescriptor(propertyName);
+
+        if (d == null)
+            return null;
+
+        checkPropertyType(d, propertyType, location);
+
+        Method write = d.getWriteMethod();
+        Method read = d.getReadMethod();
+
+        if (isImplemented(write))
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "ComponentClassFactory.non-abstract-write",
+                    write.getDeclaringClass().getName(),
+                    propertyName),
+                location,
+                null);
+
+        if (isImplemented(read))
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "ComponentClassFactory.non-abstract-read",
+                    read.getDeclaringClass().getName(),
+                    propertyName),
+                location,
+                null);
+
+        return read == null ? null : read.getName();
+    }
+
+    protected boolean isMissingProperty(String propertyName)
+    {
+        PropertyDescriptor pd = getPropertyDescriptor(propertyName);
+
+        return !isImplemented(pd);
+    }
+
+    /**
+     *  Invoked by {@link org.apache.tapestry.enhance.DefaultComponentClassEnhancer} to
+     *  create an enahanced
+     *  subclass of the component class.  This means creating a default constructor,
+     *  new fields, and new accessor and mutator methods.  Properties are created
+     *  for connected parameters, for all formal parameters (the binding property),
+     *  and for all specified parameters (which may be transient or persistent).
+     * 
+     **/
+
+    public Class createEnhancedSubclass()
+    {
+        IEnhancedClass enhancedClass = getEnhancedClass();
+
+        String startClassName = _componentClass.getName();
+        String subclassName = enhancedClass.getClassName();
+
+        if (LOG.isDebugEnabled())
+            LOG.debug(
+                "Enhancing subclass of "
+                    + startClassName
+                    + " for "
+                    + _specification.getSpecificationLocation());
+
+        Class result = enhancedClass.createEnhancedSubclass();
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Finished creating enhanced class " + subclassName);
+
+        return result;
+    }
+
+    /**
+     *  Invoked by {@link #needsEnhancement()} to find any enhancements
+     *  that may be needed.  Should create an {@link org.apache.tapestry.enhance.IEnhancer}
+     *  for each one, and add it to the queue.
+     * 
+     **/
+
+    protected void scanForEnhancements()
+    {
+        scanForParameterEnhancements();
+        scanForSpecifiedPropertyEnhancements();
+        scanForAbstractClass();
+    }
+
+    protected void scanForAbstractClass()
+    {
+        if (Modifier.isAbstract(_componentClass.getModifiers()))
+            getEnhancedClass().addEnhancer(new NoOpEnhancer());
+
+    }
+
+    /**
+     *  Invoked by {@link #scanForEnhancements()} to locate
+     *  any enhancements needed for component parameters (this includes
+     *  binding properties and connected parameter property).
+     * 
+     **/
+
+    protected void scanForParameterEnhancements()
+    {
+        List names = _specification.getParameterNames();
+        int count = names.size();
+
+        for (int i = 0; i < count; i++)
+        {
+            String name = (String) names.get(i);
+
+            IParameterSpecification ps = _specification.getParameter(name);
+
+            scanForBindingProperty(name, ps);
+
+            scanForParameterProperty(name, ps);
+        }
+
+    }
+
+    protected void scanForSpecifiedPropertyEnhancements()
+    {
+        List names = _specification.getPropertySpecificationNames();
+        int count = names.size();
+
+        for (int i = 0; i < count; i++)
+        {
+            String name = (String) names.get(i);
+
+            IPropertySpecification ps = _specification.getPropertySpecification(name);
+
+            scanForSpecifiedProperty(ps);
+        }
+    }
+
+    protected void scanForBindingProperty(String parameterName, IParameterSpecification ps)
+    {
+        String propertyName = parameterName + Tapestry.PARAMETER_PROPERTY_NAME_SUFFIX;
+        PropertyDescriptor pd = getPropertyDescriptor(propertyName);
+
+        // only enhance custom parameter binding properties if they are declared abstract
+        if (ps.getDirection() == Direction.CUSTOM)
+        {
+            if (pd == null)
+                return;
+
+            if (!(isAbstract(pd.getReadMethod()) || isAbstract(pd.getWriteMethod())))
+                return;
+        }
+
+        if (isImplemented(pd))
+            return;
+
+        // Need to create the property.
+        getEnhancedClass().createProperty(propertyName, IBinding.class.getName());
+    }
+
+    protected void scanForParameterProperty(String parameterName, IParameterSpecification ps)
+    {
+        Direction direction = ps.getDirection();
+
+        if (direction == Direction.CUSTOM)
+            return;
+
+        if (direction == Direction.AUTO)
+        {
+            addAutoParameterEnhancer(parameterName, ps);
+            return;
+        }
+
+        String propertyName = ps.getPropertyName();
+
+        // Yes, but does it *need* a property created?
+
+        if (!isMissingProperty(propertyName))
+            return;
+
+        ILocation location = ps.getLocation();
+
+        Class propertyType = convertPropertyType(ps.getType(), location);
+
+        String readMethodName = checkAccessors(propertyName, propertyType, location);
+
+        getEnhancedClass().createProperty(propertyName, ps.getType(), readMethodName, false);
+    }
+
+    protected void addAutoParameterEnhancer(String parameterName, IParameterSpecification ps)
+    {
+        ILocation location = ps.getLocation();
+        String propertyName = ps.getPropertyName();
+
+        if (!ps.isRequired() && ps.getDefaultValue() == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.format("ComponentClassFactory.auto-must-be-required", parameterName),
+                location,
+                null);
+
+        Class propertyType = convertPropertyType(ps.getType(), location);
+
+        String readMethodName = checkAccessors(propertyName, propertyType, location);
+
+        getEnhancedClass().createAutoParameter(
+            propertyName,
+            parameterName,
+            ps.getType(),
+            readMethodName);
+    }
+
+    protected void scanForSpecifiedProperty(IPropertySpecification ps)
+    {
+        String propertyName = ps.getName();
+        ILocation location = ps.getLocation();
+        Class propertyType = convertPropertyType(ps.getType(), location);
+
+        PropertyDescriptor pd = getPropertyDescriptor(propertyName);
+
+        if (isImplemented(pd))
+        {
+            // Make sure the property is at least the right type.
+
+            checkPropertyType(pd, propertyType, location);
+            return;
+        }
+
+        String readMethodName = checkAccessors(propertyName, propertyType, location);
+
+        getEnhancedClass().createProperty(
+            propertyName,
+            ps.getType(),
+            readMethodName,
+            ps.isPersistent());
+    }
+
+    public IEnhancedClass getEnhancedClass()
+    {
+        if (_enhancedClass == null)
+        {
+            String startClassName = _componentClass.getName();
+            String subclassName = startClassName + "$Enhance_" + generateUID();
+
+            // If the new class is located in a 'restricted' package, 
+            // add a neutral package prefix to the name. 
+            // The class enhancement will likely fail anyway, since the original object 
+            // would not implement IComponent, but we do not know what the enhancement
+            // will do in the future -- it might implement that interface automatically. 
+            if (subclassName.startsWith("java.") || subclassName.startsWith("javax."))
+                subclassName = PACKAGE_PREFIX + subclassName;
+
+            _enhancedClass =
+                _enhancedClassFactory.createEnhancedClass(subclassName, _componentClass);
+        }
+        return _enhancedClass;
+    }
+
+    private static synchronized int generateUID()
+    {
+        return _uid++;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/enhance/DefaultComponentClassEnhancer.java b/tapestry-framework/src/org/apache/tapestry/enhance/DefaultComponentClassEnhancer.java
new file mode 100644
index 0000000..46c38df
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/enhance/DefaultComponentClassEnhancer.java
@@ -0,0 +1,264 @@
+//  Copyright 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.tapestry.enhance;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.engine.IComponentClassEnhancer;
+import org.apache.tapestry.enhance.javassist.EnhancedClassFactory;
+import org.apache.tapestry.spec.IComponentSpecification;
+
+/**
+ *  Default implementation of {@link IComponentClassEnhancer}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ * 
+ **/
+
+public class DefaultComponentClassEnhancer implements IComponentClassEnhancer
+{
+    private static final Log LOG = LogFactory.getLog(DefaultComponentClassEnhancer.class);
+
+    /**
+     *  Map of Class, keyed on IComponentSpecification.
+     * 
+     **/
+
+    private Map _cachedClasses;
+    private IResourceResolver _resolver;
+    private IEnhancedClassFactory _factory;
+    private boolean _disableValidation;
+
+    /**
+     * @param resolver resource resolver used to locate classes
+     * @param disableValidation if true, then validation (of unimplemented abstract methods)
+     * is skipped
+     */
+    public DefaultComponentClassEnhancer(IResourceResolver resolver, boolean disableValidation)
+    {
+        _cachedClasses = Collections.synchronizedMap(new HashMap());
+        _resolver = resolver;
+        _factory = createEnhancedClassFactory();
+        _disableValidation = disableValidation;
+    }
+
+    protected IEnhancedClassFactory createEnhancedClassFactory()
+    {
+        return new EnhancedClassFactory(getResourceResolver());
+    }
+
+    public synchronized void reset()
+    {
+        _cachedClasses.clear();
+        _factory.reset();
+    }
+
+    public IResourceResolver getResourceResolver()
+    {
+        return _resolver;
+    }
+
+    public Class getEnhancedClass(IComponentSpecification specification, String className)
+    {
+            synchronized (specification)
+            {
+                Class result = getCachedClass(specification);
+
+                if (result == null)
+                {
+                    result = constructComponentClass(specification, className);
+                    storeCachedClass(specification, result);
+                }
+
+                return result;
+            }
+    }
+
+    protected void storeCachedClass(IComponentSpecification specification, Class cachedClass)
+    {
+        _cachedClasses.put(specification, cachedClass);
+    }
+
+    protected Class getCachedClass(IComponentSpecification specification)
+    {
+        return (Class) _cachedClasses.get(specification);
+    }
+
+    /**
+     *  Returns the class to be used for the component, which is either
+     *  the class with the given name, or an enhanced subclass.
+     * 
+     **/
+
+    protected Class constructComponentClass(
+        IComponentSpecification specification,
+        String className)
+    {
+        Class result = null;
+
+        try
+        {
+            result = _resolver.findClass(className);
+        }
+        catch (Exception ex)
+        {
+            throw new ApplicationRuntimeException(ex.getMessage(), specification.getLocation(), ex);
+        }
+
+        try
+        {
+            ComponentClassFactory factory = createComponentClassFactory(specification, result);
+
+            if (factory.needsEnhancement())
+            {
+                result = factory.createEnhancedSubclass();
+
+                if (!_disableValidation)
+                    validateEnhancedClass(result, className, specification);
+            }
+        }
+        catch (CodeGenerationException e)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("ComponentClassFactory.code-generation-error", className),
+                e);
+        }
+
+        return result;
+    }
+
+    /**
+     *  Constructs a new factory for enhancing the specified class. Advanced users
+     *  may want to provide thier own enhancements to classes and this method
+     *  is the hook that allows them to provide a subclass of
+     *  {@link org.apache.tapestry.enhance.ComponentClassFactory} adding those
+     *  enhancements.
+     * 
+     **/
+
+    protected ComponentClassFactory createComponentClassFactory(
+        IComponentSpecification specification,
+        Class componentClass)
+    {
+        return new ComponentClassFactory(_resolver, specification, componentClass, _factory);
+    }
+
+    /**
+     *  Invoked to validate that an enhanced class is acceptible.  Primarily, this is to ensure
+     *  that the class contains no unimplemented abstract methods or fields.  Normally,
+     *  this kind of checking is done at compile time, but for generated
+     *  classes, there is no compile time check (!) and you can get runtime
+     *  errors when accessing unimplemented abstract methods.
+     * 
+     *
+     **/
+
+    protected void validateEnhancedClass(
+        Class subject,
+        String className,
+        IComponentSpecification specification)
+    {
+        boolean debug = LOG.isDebugEnabled();
+
+        if (debug)
+            LOG.debug("Validating " + subject);
+
+        Set implementedMethods = new HashSet();
+        Class current = subject;
+
+        while (true)
+        {
+            Method m = checkForAbstractMethods(current, implementedMethods);
+
+            if (m != null)
+                throw new ApplicationRuntimeException(
+                    Tapestry.format(
+                        "DefaultComponentClassEnhancer.no-impl-for-abstract-method",
+                        new Object[] { m, current, className, subject.getName()}),
+                    specification.getLocation(),
+                    null);
+
+            // An earlier version of this code walked the interfaces directly,
+            // but it appears that implementing an interface actually
+            // puts abstract method declarations into the class
+            // (at least, in terms of what getDeclaredMethods() returns).
+
+            // March up to the super class.
+
+            current = current.getSuperclass();
+
+            // Once advanced up to a concrete class, we trust that
+            // the compiler did its checking.
+
+            if (!Modifier.isAbstract(current.getModifiers()))
+                break;
+        }
+
+    }
+
+    /**
+     *  Searches the class for abstract methods, returning the first found.
+     *  Records non-abstract methods in the implementedMethods set.
+     * 
+     **/
+
+    private Method checkForAbstractMethods(Class current, Set implementedMethods)
+    {
+        boolean debug = LOG.isDebugEnabled();
+
+        if (debug)
+            LOG.debug("Searching for abstract methods in " + current);
+
+        Method[] methods = current.getDeclaredMethods();
+
+        for (int i = 0; i < methods.length; i++)
+        {
+            Method m = methods[i];
+
+            if (debug)
+                LOG.debug("Checking " + m);
+
+            boolean isAbstract = Modifier.isAbstract(m.getModifiers());
+
+            MethodSignature s = new MethodSignature(m);
+
+            if (isAbstract)
+            {
+                if (implementedMethods.contains(s))
+                    continue;
+
+                return m;
+            }
+
+            implementedMethods.add(s);
+        }
+
+        return null;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/enhance/EnhancedClassLoader.java b/tapestry-framework/src/org/apache/tapestry/enhance/EnhancedClassLoader.java
new file mode 100644
index 0000000..2162b78
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/enhance/EnhancedClassLoader.java
@@ -0,0 +1,63 @@
+//  Copyright 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.tapestry.enhance;
+
+import java.security.ProtectionDomain;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  A class loader that can be used to create new classes 
+ *  as needed.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ * 
+ **/
+
+public class EnhancedClassLoader extends ClassLoader
+{
+
+    public EnhancedClassLoader(ClassLoader parentClassLoader)
+    {
+        super(parentClassLoader);
+    }
+
+    /**
+     *  Defines the new class.
+     * 
+     *  @throws ApplicationRuntimeException if defining the class fails.
+     * 
+     **/
+
+    public Class defineClass(String enhancedClassName, byte[] byteCode, ProtectionDomain domain)
+    {
+        try
+        {
+            return defineClass(enhancedClassName, byteCode, 0, byteCode.length, domain);
+        }
+        catch (Throwable ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "EnhancedClassLoader.unable-to-define-class",
+                    enhancedClassName,
+                    ex.getMessage()),
+                ex);
+        }
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/enhance/IEnhancedClass.java b/tapestry-framework/src/org/apache/tapestry/enhance/IEnhancedClass.java
new file mode 100644
index 0000000..d432990
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/enhance/IEnhancedClass.java
@@ -0,0 +1,54 @@
+//  Copyright 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.tapestry.enhance;
+
+/**
+ *  This interface represents a class to be enhanced. An implementation 
+ *  is generated by {@link org.apache.tapestry.enhance.IEnhancedClassFactory} 
+ *  and is specific to the selected system of enhancement.
+ * 
+ *  @author Mindbridge
+ *  @since 3.0
+ */
+public interface IEnhancedClass
+{
+    String getClassName();
+    
+    /**
+     * Adds an enhancer for creating the specified property.
+     */
+    void createProperty(String propertyName, String propertyType);
+
+    void createProperty(
+        String propertyName,
+        String propertyType,
+        String readMethodName,
+        boolean persistent);
+
+    void createAutoParameter(
+        String propertyName,
+        String parameterName,
+        String typeClassName,
+        String readMethodName);
+
+    boolean hasModifications();
+
+    Class createEnhancedSubclass();
+    
+    /**
+     * Adds an arbitrary enhancer.
+     */
+    void addEnhancer(IEnhancer enhancer);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/enhance/IEnhancedClassFactory.java b/tapestry-framework/src/org/apache/tapestry/enhance/IEnhancedClassFactory.java
new file mode 100644
index 0000000..7287270
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/enhance/IEnhancedClassFactory.java
@@ -0,0 +1,31 @@
+//  Copyright 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.tapestry.enhance;
+
+/**
+ *  An interface defining the factory for creation of new objects representing 
+ *  an enhanced class. This object is used essentially as a singleton -- there is
+ *  typically only one instance of it in the system. Common functionality, such as
+ *  caches, can be stored here.
+ * 
+ *  @author Mindbridge
+ *  @version $Id$
+ *  @since 3.0
+ */
+public interface IEnhancedClassFactory
+{
+    void reset();
+    IEnhancedClass createEnhancedClass(String className, Class parentClass);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/enhance/IEnhancer.java b/tapestry-framework/src/org/apache/tapestry/enhance/IEnhancer.java
new file mode 100644
index 0000000..b947f35
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/enhance/IEnhancer.java
@@ -0,0 +1,32 @@
+//  Copyright 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.tapestry.enhance;
+
+/**
+ *  Defines an object which may work with a 
+ *  {@link org.apache.tapestry.enhance.ComponentClassFactory}
+ *  to create an enhancement to a class.  These enhancements are
+ *  typically in the form of adding new fields and methods.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public interface IEnhancer
+{
+    public void performEnhancement(IEnhancedClass enhancedClass);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/enhance/JavaClassMapping.java b/tapestry-framework/src/org/apache/tapestry/enhance/JavaClassMapping.java
new file mode 100644
index 0000000..cc2aa18
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/enhance/JavaClassMapping.java
@@ -0,0 +1,79 @@
+//  Copyright 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.tapestry.enhance;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ *  @author Mindbridge
+ *  @version $Id$
+ *  @since 3.0
+ */
+public class JavaClassMapping
+{
+
+    /**
+     *  Map of type (as Class), keyed on type name. 
+     * 
+     **/
+
+    private Map _typeMap = new HashMap();
+
+
+    {
+        recordType("boolean", boolean.class);
+        recordType("boolean[]", boolean[].class);
+
+        recordType("short", short.class);
+        recordType("short[]", short[].class);
+
+        recordType("int", int.class);
+        recordType("int[]", int[].class);
+
+        recordType("long", long.class);
+        recordType("long[]", long[].class);
+
+        recordType("float", float.class);
+        recordType("float[]", float[].class);
+
+        recordType("double", double.class);
+        recordType("double[]", double[].class);
+
+        recordType("char", char.class);
+        recordType("char[]", char[].class);
+
+        recordType("byte", byte.class);
+        recordType("byte[]", byte[].class);
+
+        recordType("java.lang.Object", Object.class);
+        recordType("java.lang.Object[]", Object[].class);
+
+        recordType("java.lang.String", String.class);
+        recordType("java.lang.String[]", String[].class);
+    }
+
+
+    public void recordType(String name, Class type)
+    {
+        _typeMap.put(name, type);
+    }
+
+    public Class getType(String name)
+    {
+        return (Class) _typeMap.get(name);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/enhance/MethodSignature.java b/tapestry-framework/src/org/apache/tapestry/enhance/MethodSignature.java
new file mode 100644
index 0000000..cabbe9e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/enhance/MethodSignature.java
@@ -0,0 +1,102 @@
+//  Copyright 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.tapestry.enhance;
+
+import java.lang.reflect.Method;
+
+import org.apache.commons.lang.builder.EqualsBuilder;
+import org.apache.commons.lang.builder.HashCodeBuilder;
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+/**
+ *  The signature of a {@link java.lang.reflect.Method}, including
+ *  the name, return type, and parameter types.  Used when checking
+ *  for unimplemented methods in enhanced subclasses.  
+ * 
+ *  <p>
+ *  The modifiers (i.e., "public", "abstract") and thrown
+ *  exceptions are not relevant for these purposes, and
+ *  are not part of the signature.
+ * 
+ *  <p>
+ *  Instances of MethodSignature are immutable and
+ *  implement equals() and hashCode() properly for use
+ *  in Sets or as Map keys.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class MethodSignature
+{
+    private String _name;
+    private Class _returnType;
+    private Class[] _parameterTypes;
+    private int _hashCode = 0;
+
+    public MethodSignature(Method m)
+    {
+        _name = m.getName();
+        _returnType = m.getReturnType();
+
+        // getParameterTypes() returns a copy for us to keep.
+
+        _parameterTypes = m.getParameterTypes();
+    }
+
+    public boolean equals(Object obj)
+    {
+        if (obj == null || !(obj instanceof MethodSignature))
+            return false;
+
+        MethodSignature other = (MethodSignature) obj;
+
+        EqualsBuilder builder = new EqualsBuilder();
+        builder.append(_name, other._name);
+        builder.append(_returnType, other._returnType);
+        builder.append(_parameterTypes, other._parameterTypes);
+
+        return builder.isEquals();
+    }
+
+    public int hashCode()
+    {
+        if (_hashCode == 0)
+        {
+            HashCodeBuilder builder = new HashCodeBuilder(253, 97);
+
+            builder.append(_name);
+            builder.append(_returnType);
+            builder.append(_parameterTypes);
+
+            _hashCode = builder.toHashCode();
+        }
+
+        return _hashCode;
+    }
+
+    public String toString()
+    {
+        ToStringBuilder builder = new ToStringBuilder(this);
+        builder.append("name", _name);
+        builder.append("returnType", _returnType);
+        builder.append("parameterTypes", _parameterTypes);
+
+        return builder.toString();
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/enhance/NoOpEnhancer.java b/tapestry-framework/src/org/apache/tapestry/enhance/NoOpEnhancer.java
new file mode 100644
index 0000000..d5b9c09
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/enhance/NoOpEnhancer.java
@@ -0,0 +1,35 @@
+//  Copyright 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.tapestry.enhance;
+
+/**
+ * Does nothing; added to all abstract classes to force them to be subclassed as concrete
+ * (even if no other enhancement takes place).
+ *
+ * @author Howard Lewis Ship
+ * @sincd 3.0
+ */
+public class NoOpEnhancer implements IEnhancer
+{
+
+	/**
+	 * Does nothing.
+	 */
+    public void performEnhancement(IEnhancedClass enhancedClass)
+    {
+
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/enhance/javassist/ClassFabricator.java b/tapestry-framework/src/org/apache/tapestry/enhance/javassist/ClassFabricator.java
new file mode 100644
index 0000000..926ae98
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/enhance/javassist/ClassFabricator.java
@@ -0,0 +1,285 @@
+//  Copyright 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.tapestry.enhance.javassist;
+
+import javassist.*;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.enhance.CodeGenerationException;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+
+/**
+ *  @author Mindbridge
+ *  @version $Id$
+ *  @since 3.0
+ */
+public class ClassFabricator
+{
+    private static final Log LOG = LogFactory.getLog(ClassFabricator.class);
+
+    /**
+     *  The code template for the standard property accessor method.    
+     *                                      <p>
+     *  Legend:                             <br>
+     *      {0} = property field name       <br>
+     */
+    private static final String PROPERTY_ACCESSOR_TEMPLATE = "" +
+        "'{'" +
+        "  return {0}; " +
+        "'}'";
+        
+    /**
+     *  The code template for the standard property mutator method.    
+     *                                      <p>
+     *  Legend:                             <br>
+     *      {0} = property field name       <br>
+     */
+    private static final String PROPERTY_MUTATOR_TEMPLATE = "" +
+        "'{'" +
+        "  {0} = $1; " +
+        "'}'";
+        
+    /**
+     *  The code template for the standard persistent property mutator method.    
+     *                                      <p>
+     *  Legend:                             <br>
+     *      {0} = property field name       <br>
+     *      {1} = property name             <br>
+     */
+    private static final String PERSISTENT_PROPERTY_MUTATOR_TEMPLATE =
+        "" +
+        "'{'" +
+        "  {0} = $1;" +
+        "  fireObservedChange(\"{1}\", {0}); " +
+        "'}'";
+
+    private ClassPool _classPool;
+    private CtClass _genClass;
+
+    public ClassFabricator(String className, CtClass parentClass, ClassPool classPool)
+    {
+        _classPool = classPool;
+        _genClass = _classPool.makeClass(className, parentClass);
+    }
+
+    public CtField getField(String fieldName)
+    {
+        try
+        {
+            return _genClass.getField(fieldName);
+        }
+        catch (NotFoundException e)
+        {
+            return null;
+        }
+    }
+    
+    public void createField(CtClass fieldType, String fieldName)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Creating field: " + fieldName);
+
+        try
+        {
+            CtField field = new CtField(fieldType, fieldName, _genClass);
+            _genClass.addField(field);
+        }
+        catch (CannotCompileException e)
+        {
+            throw new CodeGenerationException(e);
+        }
+    }
+
+    public void createField(CtClass fieldType, String fieldName, String init)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Creating field: " + fieldName + " with initializer: " + init);
+
+        try
+        {
+            CtField field = new CtField(fieldType, fieldName, _genClass);
+            _genClass.addField(field, init);
+        }
+        catch (CannotCompileException e)
+        {
+            throw new CodeGenerationException(e);
+        }
+    }
+
+    public CtMethod getMethod(String name, String signature)
+    {
+        try
+        {
+            return _genClass.getMethod(name, signature);
+        }
+        catch (NotFoundException e)
+        {
+            return null;
+        }
+    }
+
+    public void addMethod(CtMethod method) throws CannotCompileException
+    {
+        _genClass.addMethod(method);
+    }
+
+    /**
+     *  Constructs an accessor method name.
+     * 
+     **/
+
+    public String buildMethodName(String prefix, String propertyName)
+    {
+        StringBuffer result = new StringBuffer(prefix);
+
+        char ch = propertyName.charAt(0);
+
+        result.append(Character.toUpperCase(ch));
+
+        result.append(propertyName.substring(1));
+
+        return result.toString();
+    }
+
+    public CtMethod createMethod(
+        CtClass returnType,
+        String methodName,
+        CtClass[] arguments)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Creating method: " + methodName);
+
+        CtMethod method = new CtMethod(returnType, methodName, arguments, _genClass);
+
+        return method;
+    }
+
+    public CtMethod createAccessor(
+        CtClass fieldType,
+        String propertyName,
+        String readMethodName)
+    {
+        String methodName =
+            readMethodName == null ? buildMethodName("get", propertyName) : readMethodName;
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Creating accessor: " + methodName);
+
+        CtMethod method = new CtMethod(fieldType, methodName, new CtClass[0], _genClass);
+
+        return method;
+    }
+
+    /**
+     *  Creates an accessor (getter) method for the property.
+     * 
+     *  @param fieldType the return type for the method
+     *  @param fieldName the name of the field (not the name of the property)
+     *  @param propertyName the name of the property (used to build the name of the method)
+     *  @param readMethodName if not null, the name of the method to use
+     * 
+     **/
+
+    public void createPropertyAccessor(
+        CtClass fieldType,
+        String fieldName,
+        String propertyName,
+        String readMethodName)
+    {
+        try
+        {
+            String accessorBody =
+                MessageFormat.format(PROPERTY_ACCESSOR_TEMPLATE, new Object[] { fieldName, propertyName });
+
+            CtMethod method = createAccessor(fieldType, propertyName, readMethodName);
+            method.setBody(accessorBody);
+            _genClass.addMethod(method);
+        }
+        catch (CannotCompileException e)
+        {
+            throw new CodeGenerationException(e);
+        }
+    }
+
+    public CtMethod createMutator(
+        CtClass fieldType,
+        String propertyName)
+    {
+        String methodName = buildMethodName("set", propertyName);
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Creating mutator: " + methodName);
+
+        CtMethod method =
+            new CtMethod(CtClass.voidType, methodName, new CtClass[] { fieldType }, _genClass);
+
+        return method;
+    }
+
+    /**
+     *  Creates a mutator (aka "setter") method.
+     * 
+     *  @param fieldType type of field value (and type of parameter value)
+     *  @param fieldName name of field (not property!)
+     *  @param propertyName name of property (used to construct method name)
+     *  @param isPersistent if true, adds a call to fireObservedChange()
+     * 
+     **/
+
+    public void createPropertyMutator(
+        CtClass fieldType,
+        String fieldName,
+        String propertyName,
+        boolean isPersistent)
+    {
+        String bodyTemplate = isPersistent ? PERSISTENT_PROPERTY_MUTATOR_TEMPLATE : PROPERTY_MUTATOR_TEMPLATE;
+        String body = MessageFormat.format(bodyTemplate, new Object[] { fieldName, propertyName });
+
+        try
+        {
+            CtMethod method = createMutator(fieldType, propertyName);
+            method.setBody(body);
+            _genClass.addMethod(method);
+        }
+        catch (CannotCompileException e)
+        {
+            throw new CodeGenerationException(e);
+        }
+    }
+
+    
+    public void commit()
+    {
+    }
+
+    public byte[] getByteCode()
+    {
+        try
+        {
+            return _genClass.toBytecode();
+        }
+        catch (IOException e)
+        {
+            throw new CodeGenerationException(e);
+        }
+        catch (CannotCompileException e)
+        {
+            throw new CodeGenerationException(e);
+        }
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/enhance/javassist/ClassMapping.java b/tapestry-framework/src/org/apache/tapestry/enhance/javassist/ClassMapping.java
new file mode 100644
index 0000000..6c99b83
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/enhance/javassist/ClassMapping.java
@@ -0,0 +1,102 @@
+//  Copyright 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.tapestry.enhance.javassist;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javassist.ClassPool;
+import javassist.CtClass;
+import javassist.NotFoundException;
+
+import org.apache.tapestry.enhance.CodeGenerationException;
+
+/**
+ *  @author Mindbridge
+ *  @version $Id$
+ *  @since 3.0
+ */
+public class ClassMapping
+{
+
+    /**
+     *  Map of type (as Type), keyed on type name.
+     * 
+     *  This should be kept in synch with ParameterManager, which maintains
+     *  a similar list.
+     * 
+     **/
+
+    private Map _objectTypeMap = new HashMap();
+    private ClassPool _classPool;
+
+    public ClassMapping(ClassPool classPool)
+    {
+        _classPool = classPool;
+        initialize();
+    }
+
+    protected void initialize()
+    {
+        recordType("boolean", CtClass.booleanType);
+        recordType("short", CtClass.shortType);
+        recordType("int", CtClass.intType);
+        recordType("long", CtClass.longType);
+        recordType("float", CtClass.floatType);
+        recordType("double", CtClass.doubleType);
+        recordType("char", CtClass.charType);
+        recordType("byte", CtClass.byteType);
+        
+        try
+        {
+            loadType("boolean[]");
+            loadType("short[]");
+            loadType("int[]");
+            loadType("long[]");
+            loadType("float[]");
+            loadType("double[]");
+            loadType("char[]");
+            loadType("byte[]");
+
+            loadType("java.lang.Object");
+            loadType("java.lang.Object[]");
+
+            loadType("java.lang.String");
+            loadType("java.lang.String[]");
+        }
+        catch (NotFoundException e)
+        {
+            // This exception should not occur since the types above must exist.
+            // Nevertheless...
+            throw new CodeGenerationException(e);
+        }
+    }
+
+    public void loadType(String type) throws NotFoundException
+    {
+        CtClass objectType = _classPool.get(type);
+        _objectTypeMap.put(type, objectType);
+    }
+    
+    public void recordType(String type, CtClass objectType)
+    {
+        _objectTypeMap.put(type, objectType);
+    }
+
+    public CtClass getType(String type)
+    {
+        return (CtClass) _objectTypeMap.get(type);
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/enhance/javassist/CreateAutoParameterEnhancer.java b/tapestry-framework/src/org/apache/tapestry/enhance/javassist/CreateAutoParameterEnhancer.java
new file mode 100644
index 0000000..d1bafc7
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/enhance/javassist/CreateAutoParameterEnhancer.java
@@ -0,0 +1,220 @@
+//  Copyright 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.tapestry.enhance.javassist;
+
+import java.text.MessageFormat;
+import java.util.HashMap;
+import java.util.Map;
+
+import javassist.CannotCompileException;
+import javassist.CtClass;
+import javassist.CtMethod;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.enhance.CodeGenerationException;
+import org.apache.tapestry.enhance.IEnhancedClass;
+import org.apache.tapestry.enhance.IEnhancer;
+
+/**
+ *  Creates a synthetic property for a
+ *  {@link org.apache.tapestry.spec.Direction#AUTO}
+ *  parameter.
+ *
+ *  @author Mindbridge
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class CreateAutoParameterEnhancer implements IEnhancer
+{
+    private static final Log LOG = LogFactory.getLog(CreateAutoParameterEnhancer.class);
+
+    /**
+     *  The code template for the parameter accessor method.    
+     *                                      <p>
+     *  Legend:                             <br>
+     *      {0} = readBindingMethodName     <br>
+     *      {1} = binding value accessor    <br>
+     *      {2} = cast (if needed)          <br>
+     */
+    protected static final String PARAMETER_ACCESSOR_TEMPLATE =
+        ""
+            + "'{'"
+            + "  org.apache.tapestry.IBinding binding = {0}();"
+            + "  return {2} binding.{1}(); "
+            + "'}'";
+
+    /**
+     *  The code template for the parameter mutator method.    
+     *                                      <p>
+     *  Legend:                             <br>
+     *      {0} = readBindingMethodName     <br>
+     *      {1} = binding value mutator     <br>
+     *      {2} = value cast
+     */
+    protected static final String PARAMETER_MUTATOR_TEMPLATE =
+        ""
+            + "'{'"
+            + "  org.apache.tapestry.IBinding binding = {0}();"
+            + "  binding.{1}({2} $1); "
+            + "'}'";
+
+    /**
+     *  The list of types that have accessors and mutators 
+     *  other than getObject()/setObject.
+     *  The key in the Map is the type, the value is the property name in IBinding 
+     */
+    private static final Map SPECIAL_BINDING_TYPES = new HashMap();
+
+    static {
+        SPECIAL_BINDING_TYPES.put("boolean", "boolean");
+        SPECIAL_BINDING_TYPES.put("int", "int");
+        SPECIAL_BINDING_TYPES.put("double", "double");
+        SPECIAL_BINDING_TYPES.put("java.lang.String", "string");
+    }
+
+    private static final Map VALUE_CAST_TYPES = new HashMap();
+
+    static {
+        VALUE_CAST_TYPES.put("byte", "($w)");
+        VALUE_CAST_TYPES.put("long", "($w)");
+        VALUE_CAST_TYPES.put("short", "($w)");
+        VALUE_CAST_TYPES.put("char", "($w)");
+        VALUE_CAST_TYPES.put("float", "($w)");
+    }
+
+    private EnhancedClass _enhancedClass;
+    private String _propertyName;
+    private String _parameterName;
+    private CtClass _type;
+    private String _readMethodName;
+
+    public CreateAutoParameterEnhancer(
+        EnhancedClass enhancedClass,
+        String propertyName,
+        String parameterName,
+        CtClass type,
+        String readMethodName)
+    {
+        _enhancedClass = enhancedClass;
+        _propertyName = propertyName;
+        _parameterName = parameterName;
+        _type = type;
+        _readMethodName = readMethodName;
+    }
+
+    public void performEnhancement(IEnhancedClass enhancedClass)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Creating auto property: " + _propertyName);
+
+        EnhancedClass jaEnhancedClass = (EnhancedClass) enhancedClass;
+        ClassFabricator cf = jaEnhancedClass.getClassFabricator();
+
+        String readBindingMethodName =
+            cf.buildMethodName("get", _parameterName + Tapestry.PARAMETER_PROPERTY_NAME_SUFFIX);
+
+        createReadMethod(cf, readBindingMethodName);
+        createWriteMethod(cf, readBindingMethodName);
+    }
+
+    private String getSpecialBindingType()
+    {
+        String typeName = _type.getName();
+        return (String) SPECIAL_BINDING_TYPES.get(typeName);
+    }
+
+    private String getValueCastType()
+    {
+        String typeName = _type.getName();
+
+        return (String) VALUE_CAST_TYPES.get(typeName);
+    }
+
+    private void createReadMethod(ClassFabricator cf, String readBindingMethodName)
+    {
+        String castToType;
+        String bindingValueAccessor;
+
+        String specialBindingType = getSpecialBindingType();
+        if (specialBindingType != null)
+        {
+            castToType = "";
+            bindingValueAccessor = cf.buildMethodName("get", specialBindingType);
+        }
+        else
+        {
+            castToType = "($r)";
+            bindingValueAccessor = "getObject";
+        }
+
+        String readMethodBody =
+            MessageFormat.format(
+                PARAMETER_ACCESSOR_TEMPLATE,
+                new Object[] { readBindingMethodName, bindingValueAccessor, castToType });
+
+        try
+        {
+            CtMethod method = cf.createAccessor(_type, _propertyName, _readMethodName);
+            method.setBody(readMethodBody);
+            cf.addMethod(method);
+        }
+        catch (CannotCompileException e)
+        {
+            throw new CodeGenerationException(e);
+        }
+    }
+
+    private void createWriteMethod(ClassFabricator cf, String readBindingMethodName)
+    {
+        String bindingValueAccessor;
+        String valueCast = "";
+
+        String specialBindingType = getSpecialBindingType();
+        if (specialBindingType != null)
+        {
+            bindingValueAccessor = cf.buildMethodName("set", specialBindingType);
+        }
+        else
+        {
+            bindingValueAccessor = "setObject";
+
+            String castForType = getValueCastType();
+
+            if (castForType != null)
+                valueCast = castForType;
+        }
+
+        String writeMethodBody =
+            MessageFormat.format(
+                PARAMETER_MUTATOR_TEMPLATE,
+                new Object[] { readBindingMethodName, bindingValueAccessor, valueCast });
+
+        try
+        {
+            CtMethod method = cf.createMutator(_type, _propertyName);
+            method.setBody(writeMethodBody);
+            cf.addMethod(method);
+        }
+        catch (CannotCompileException e)
+        {
+            throw new CodeGenerationException(e);
+        }
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/enhance/javassist/CreatePropertyEnhancer.java b/tapestry-framework/src/org/apache/tapestry/enhance/javassist/CreatePropertyEnhancer.java
new file mode 100644
index 0000000..ad16879
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/enhance/javassist/CreatePropertyEnhancer.java
@@ -0,0 +1,63 @@
+//  Copyright 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.tapestry.enhance.javassist;
+
+import javassist.CtClass;
+
+import org.apache.tapestry.enhance.IEnhancedClass;
+import org.apache.tapestry.enhance.IEnhancer;
+
+/**
+ *  @author Mindbridge
+ *  @version $Id$
+ *  @since 3.0
+ */
+public class CreatePropertyEnhancer implements IEnhancer
+{
+    private String _propertyName;
+    private CtClass _propertyType;
+    private boolean _persistent;
+    private String _readMethodName;
+
+    public CreatePropertyEnhancer(String propertyName, CtClass propertyType)
+    {
+        this(propertyName, propertyType, null, false);
+    }
+
+    public CreatePropertyEnhancer(
+        String propertyName,
+        CtClass propertyType,
+        String readMethodName,
+        boolean persistent)
+    {
+        _propertyName = propertyName;
+        _propertyType = propertyType;
+        _readMethodName = readMethodName;
+        _persistent = persistent;
+    }
+
+    public void performEnhancement(IEnhancedClass enhancedClass)
+    {
+        String fieldName = "_$" + _propertyName;
+
+        EnhancedClass jaEnhancedClass = (EnhancedClass) enhancedClass;
+        ClassFabricator classFabricator = jaEnhancedClass.getClassFabricator(); 
+
+        classFabricator.createField(_propertyType, fieldName);
+        classFabricator.createPropertyAccessor(_propertyType, fieldName, _propertyName, _readMethodName);
+        classFabricator.createPropertyMutator(_propertyType, fieldName, _propertyName, _persistent);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/enhance/javassist/EnhancedClass.java b/tapestry-framework/src/org/apache/tapestry/enhance/javassist/EnhancedClass.java
new file mode 100644
index 0000000..eef0adc
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/enhance/javassist/EnhancedClass.java
@@ -0,0 +1,139 @@
+//  Copyright 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.tapestry.enhance.javassist;
+
+import javassist.ClassPool;
+import javassist.CtClass;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.enhance.BaseEnhancedClass;
+import org.apache.tapestry.enhance.EnhancedClassLoader;
+import org.apache.tapestry.enhance.IEnhancer;
+
+/**
+ *  Represents a class to be enhanced using Javassist. 
+ * 
+ *  @author Mindbridge
+ *  @version $Id$
+ *  @since 3.0
+ */
+public class EnhancedClass extends BaseEnhancedClass
+{
+    private static final Log LOG = LogFactory.getLog(EnhancedClass.class);
+
+    private String _className;
+    private Class _parentClass;
+    private EnhancedClassFactory _classFactory;
+
+    private ClassFabricator _classFabricator = null;
+
+    public EnhancedClass(String className, Class parentClass, EnhancedClassFactory classFactory)
+    {
+        _className = className;
+        _parentClass = parentClass;
+        _classFactory = classFactory;
+    }
+
+    /**
+     * @see org.apache.tapestry.enhance.IEnhancedClass#getClassName()
+     */
+    public String getClassName()
+    {
+        return _className;
+    }
+
+    public CtClass getObjectType(String type)
+    {
+        return _classFactory.getObjectType(type);
+    }
+
+    public ClassFabricator getClassFabricator()
+    {
+        if (_classFabricator == null)
+        {
+            CtClass jaParentClass = getObjectType(_parentClass.getName());
+            ClassPool classPool = _classFactory.getClassPool();
+            _classFabricator = new ClassFabricator(_className, jaParentClass, classPool);
+        }
+        return _classFabricator;
+    }
+
+    /**
+     * @see org.apache.tapestry.enhance.IEnhancedClass#createProperty(java.lang.String, java.lang.String)
+     */
+    public void createProperty(String propertyName, String propertyType)
+    {
+        createProperty(propertyName, propertyType, null, false);
+    }
+
+    /**
+     * @see org.apache.tapestry.enhance.IEnhancedClass#createProperty(java.lang.String, java.lang.String, java.lang.String, boolean)
+     */
+    public void createProperty(
+        String propertyName,
+        String propertyType,
+        String readMethodName,
+        boolean persistent)
+    {
+        IEnhancer enhancer =
+            new CreatePropertyEnhancer(
+                propertyName,
+                getObjectType(propertyType),
+                readMethodName,
+                persistent);
+        addEnhancer(enhancer);
+    }
+
+    /**
+     * @see org.apache.tapestry.enhance.IEnhancedClass#createAutoParameter(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
+     */
+    public void createAutoParameter(
+        String propertyName,
+        String parameterName,
+        String typeClassName,
+        String readMethodName)
+    {
+        IEnhancer enhancer =
+            new CreateAutoParameterEnhancer(
+                this,
+                propertyName,
+                parameterName,
+                getObjectType(typeClassName),
+                readMethodName);
+        addEnhancer(enhancer);
+    }
+
+    /**
+     * @see org.apache.tapestry.enhance.IEnhancedClass#createEnhancedSubclass()
+     */
+    public Class createEnhancedSubclass()
+    {
+        performEnhancement();
+
+        ClassFabricator cf = getClassFabricator();
+        cf.commit();
+
+        String enhancedClassName = getClassName();
+        byte[] enhancedClassBytes = cf.getByteCode();
+
+        EnhancedClassLoader loader = _classFactory.getEnhancedClassLoader();
+        return loader.defineClass(
+            enhancedClassName,
+            enhancedClassBytes,
+            _parentClass.getProtectionDomain());
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/enhance/javassist/EnhancedClassFactory.java b/tapestry-framework/src/org/apache/tapestry/enhance/javassist/EnhancedClassFactory.java
new file mode 100644
index 0000000..d858219
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/enhance/javassist/EnhancedClassFactory.java
@@ -0,0 +1,134 @@
+//  Copyright 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.tapestry.enhance.javassist;
+
+import javassist.ClassPool;
+import javassist.CtClass;
+import javassist.LoaderClassPath;
+import javassist.NotFoundException;
+
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.enhance.CodeGenerationException;
+import org.apache.tapestry.enhance.EnhancedClassLoader;
+import org.apache.tapestry.enhance.IEnhancedClass;
+import org.apache.tapestry.enhance.IEnhancedClassFactory;
+
+/**
+ *  This class defines the factory for creation of new Javassist enhanced classes. 
+ *  There is typically only one object of this class in the system. 
+ *  Common functionality objects for Javassist enhancement are stored here.
+ * 
+ *  @author Mindbridge
+ *  @version $Id$
+ *  @since 3.0
+ */
+public class EnhancedClassFactory implements IEnhancedClassFactory
+{
+    private IResourceResolver _resourceResolver;
+    private EnhancedClassLoader _enhancedClassLoader;
+    private ClassPool _classPool;
+
+    private ClassMapping _typeMap = null;
+
+    public EnhancedClassFactory(IResourceResolver resourceResolver)
+    {
+        _resourceResolver = resourceResolver;
+
+        reset();
+    }
+    
+    protected ClassPool createClassPool()
+    {
+        ClassLoader loader = _resourceResolver.getClassLoader();
+        
+        // create a new ClassPool and make sure it uses the application resource resolver 
+        ClassPool classPool = new ClassPool(null);
+        classPool.insertClassPath(new LoaderClassPath(loader));
+        
+        return classPool;
+    }
+    
+    /**
+     * @see org.apache.tapestry.enhance.IEnhancedClassFactory#reset()
+     */
+    public synchronized void reset()
+    {
+        // create a new class pool and discard the previous one
+        _classPool = createClassPool();
+        _typeMap = new ClassMapping(_classPool);
+
+        ClassLoader loader = _resourceResolver.getClassLoader();
+        _enhancedClassLoader = new EnhancedClassLoader(loader);
+    }
+   
+
+    /**
+     * @see org.apache.tapestry.enhance.IEnhancedClassFactory#createEnhancedClass(java.lang.String, java.lang.Class)
+     */
+    public IEnhancedClass createEnhancedClass(String className, Class parentClass)
+    {
+        return new EnhancedClass(className, parentClass, this);
+    }
+
+    public ClassPool getClassPool()
+    {
+        return _classPool;
+    }
+
+    public ClassMapping getClassMapping()
+    {
+        return _typeMap;
+    }
+
+    /**
+     *  Given the java class, returns the equivalent {@link CtClass type}.  In addition,
+     *  knows about scalar types, arrays of scalar types, java.lang.Object[] and
+     *  java.lang.String[].
+     * 
+     **/
+
+    public CtClass getObjectType(String type)
+    {
+
+            synchronized (this) {
+                CtClass result = getClassMapping().getType(type);
+
+                if (result == null)
+                {
+                    try
+                    {
+                        result = _classPool.get(type);
+                        getClassMapping().recordType(type, result);
+                    }
+                    catch (NotFoundException e)
+                    {
+                        throw new CodeGenerationException(e);
+                    }
+                }
+                return result;
+            }
+
+    }
+
+
+    /**
+     * @return The class loader to be used to create the enhanced class
+     */
+    public EnhancedClassLoader getEnhancedClassLoader()
+    {
+        return _enhancedClassLoader;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/enhance/package.html b/tapestry-framework/src/org/apache/tapestry/enhance/package.html
new file mode 100644
index 0000000..390492b
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/enhance/package.html
@@ -0,0 +1,14 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+Classes used for performing dynamic bytecode enhancement of component and page classes.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/event/ChangeObserver.java b/tapestry-framework/src/org/apache/tapestry/event/ChangeObserver.java
new file mode 100644
index 0000000..8f7b632
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/event/ChangeObserver.java
@@ -0,0 +1,37 @@
+//  Copyright 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.tapestry.event;
+
+/**
+ * May observe changes in an object's properties.  This is a "weak" variation
+ * on JavaBean's style bound properties.  It is used when there will be at most
+ * a single listener on property changes, and that the listener is not interested
+ * in the old value.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * 
+ **/
+
+public interface ChangeObserver
+{
+    /**
+     *  Sent when the observed object changes a property.  The event identifies
+     *  the object, the property and the new value.
+     *
+     **/
+
+    public void observeChange(ObservedChangeEvent event);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/event/ObservedChangeEvent.java b/tapestry-framework/src/org/apache/tapestry/event/ObservedChangeEvent.java
new file mode 100644
index 0000000..3adcf22
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/event/ObservedChangeEvent.java
@@ -0,0 +1,143 @@
+//  Copyright 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.tapestry.event;
+
+import java.util.EventObject;
+
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.Tapestry;
+
+/**
+ * Event which describes a change to a particular {@link IComponent}.
+ *
+ * @author Howard Ship
+ * @version $Id$
+ * 
+ **/
+
+public class ObservedChangeEvent extends EventObject
+{
+    private IComponent _component;
+    private String _propertyName;
+    private Object _newValue;
+
+	/**
+	 * @deprecated To be removed in 3.1. Use {@link #ObservedChangeEvent(IComponent, String, Object)} instead.
+	 */
+    public ObservedChangeEvent(IComponent component, String propertyName, char newValue)
+    {
+        this(component, propertyName, new Character(newValue));
+    }
+
+	/**
+	 * @deprecated To be removed in 3.1. Use {@link #ObservedChangeEvent(IComponent, String, Object)} instead.
+	 */
+    public ObservedChangeEvent(IComponent component, String propertyName, byte newValue)
+    {
+        this(component, propertyName, new Byte(newValue));
+    }
+
+	/**
+	 * @deprecated To be removed in 3.1. Use {@link #ObservedChangeEvent(IComponent, String, Object)} instead.
+	 */
+    public ObservedChangeEvent(IComponent component, String propertyName, short newValue)
+    {
+        this(component, propertyName, new Short(newValue));
+    }
+
+	/**
+	 * @deprecated To be removed in 3.1. Use {@link #ObservedChangeEvent(IComponent, String, Object)} instead.
+	 */
+    public ObservedChangeEvent(IComponent component, String propertyName, int newValue)
+    {
+        this(component, propertyName, new Integer(newValue));
+    }
+
+	/**
+	 * @deprecated To be removed in 3.1. Use {@link #ObservedChangeEvent(IComponent, String, Object)} instead.
+	 */
+    public ObservedChangeEvent(IComponent component, String propertyName, long newValue)
+    {
+        this(component, propertyName, new Long(newValue));
+    }
+
+	/**
+	 * @deprecated To be removed in 3.1. Use {@link #ObservedChangeEvent(IComponent, String, Object)} instead.
+	 */
+   public ObservedChangeEvent(IComponent component, String propertyName, double newValue)
+    {
+        this(component, propertyName, new Double(newValue));
+    }
+
+	/**
+	 * @deprecated To be removed in 3.1. Use {@link #ObservedChangeEvent(IComponent, String, Object)} instead.
+	 */
+   public ObservedChangeEvent(IComponent component, String propertyName, float newValue)
+    {
+        this(component, propertyName, new Float(newValue));
+    }
+
+    /**
+     *  Creates the event.  The new value must be null, or be a serializable object.
+     *  (It is declared as Object as a concession to the Java 2 collections framework, where
+     *  the implementations are serializable but the interfaces (Map, List, etc.) don't
+     *  extend Serializable ... so we wait until runtime to check).
+     *
+     *  @param component The component (not necessarily a page) whose property changed.
+     *  @param propertyName the name of the property which was changed.
+     *  @param newValue The new value of the property. 
+     *
+     *  @throws IllegalArgumentException if propertyName is null, or
+     *  if the new value is not serializable
+     *
+     **/
+
+    public ObservedChangeEvent(IComponent component, String propertyName, Object newValue)
+    {
+        super(component);
+
+        if (propertyName == null)
+            throw new IllegalArgumentException(
+                Tapestry.format("ObservedChangeEvent.null-property-name", component));
+
+        _component = component;
+        _propertyName = propertyName;
+        _newValue = newValue;
+    }
+
+	/**
+	 * @deprecated To be removed in 3.1. Use {@link #ObservedChangeEvent(IComponent, String, Object)} instead.
+	 */
+    public ObservedChangeEvent(IComponent component, String propertyName, boolean newValue)
+    {
+        this(component, propertyName, newValue ? Boolean.TRUE : Boolean.FALSE);
+    }
+
+    public IComponent getComponent()
+    {
+        return _component;
+    }
+
+    public Object getNewValue()
+    {
+        return _newValue;
+    }
+
+    public String getPropertyName()
+    {
+        return _propertyName;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/event/PageDetachListener.java b/tapestry-framework/src/org/apache/tapestry/event/PageDetachListener.java
new file mode 100644
index 0000000..f9c79b1
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/event/PageDetachListener.java
@@ -0,0 +1,37 @@
+//  Copyright 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.tapestry.event;
+
+import java.util.EventListener;
+
+/**
+ *  An interface for objects that want to know when the end of the
+ *  request cycle occurs, so that any resources that should be limited
+ *  to just one request cycle can be released.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.5
+ **/
+
+public interface PageDetachListener extends EventListener
+{
+    /**
+     *  Invoked by the page from its {@link org.apache.tapestry.IPage#detach()} method.
+     *
+     **/
+
+    public void pageDetached(PageEvent event);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/event/PageEvent.java b/tapestry-framework/src/org/apache/tapestry/event/PageEvent.java
new file mode 100644
index 0000000..eaa3335
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/event/PageEvent.java
@@ -0,0 +1,61 @@
+//  Copyright 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.tapestry.event;
+
+import java.util.EventObject;
+
+import org.apache.tapestry.IPage;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  Encapsulates information related to the page listener
+ *  interfaces.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.5
+ * 
+ **/
+
+public class PageEvent extends EventObject
+{
+    private transient IPage page;
+    private transient IRequestCycle requestCycle;
+
+    /**
+     *  Constructs a new instance of the event.  The
+     *  {@link EventObject#getSource()} of the event will
+     *  be the {@link IPage}.
+     *
+     **/
+
+    public PageEvent(IPage page, IRequestCycle cycle)
+    {
+        super(page);
+
+        this.page = page;
+        this.requestCycle = cycle;
+    }
+
+    public IPage getPage()
+    {
+        return page;
+    }
+
+    public IRequestCycle getRequestCycle()
+    {
+        return requestCycle;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/event/PageRenderListener.java b/tapestry-framework/src/org/apache/tapestry/event/PageRenderListener.java
new file mode 100644
index 0000000..1a2fd99
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/event/PageRenderListener.java
@@ -0,0 +1,52 @@
+//  Copyright 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.tapestry.event;
+
+import java.util.EventListener;
+
+/**
+ *  An object that listens to page events.  The {@link org.apache.tapestry.IPage page} generates
+ *  events before and after rendering a response.  These events also occur before and
+ *  after a form rewinds.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.5
+ * 
+ **/
+
+public interface PageRenderListener extends EventListener
+{
+    /**
+     *  Invoked before just before the page renders a response.  This provides
+     *  listeners with a last chance to initialize themselves for the render.
+     *  This initialization can include modifying peristent page properties.
+     *
+     *
+     **/
+
+    public void pageBeginRender(PageEvent event);
+
+    /**
+     *  Invoked after a successful render of the page.
+     *  Allows objects to release any resources they needed during the
+     *  the render.
+     * 
+     *  @see org.apache.tapestry.AbstractComponent#pageEndRender(PageEvent)
+     *
+     **/
+
+    public void pageEndRender(PageEvent event);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/event/PageValidateListener.java b/tapestry-framework/src/org/apache/tapestry/event/PageValidateListener.java
new file mode 100644
index 0000000..2c3087e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/event/PageValidateListener.java
@@ -0,0 +1,38 @@
+//  Copyright 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.tapestry.event;
+
+import java.util.EventListener;
+
+/**
+ *  An interface for objects that want to take part in the validation of the page.
+ *
+ *  @author Mindbridge
+ *  @version $Id$
+ *  @since 3.0
+ **/
+
+public interface PageValidateListener extends EventListener
+{
+    /**
+     *  Invoked by the page from its
+     *  {@link org.apache.tapestry.IPage#validate(org.apache.tapestry.IRequestCycle)} method.
+     *
+     *  <p>May throw a {@link org.apache.tapestry.PageRedirectException}, to redirect the user
+     *  to an appropriate part of the system (such as, a login page).
+     **/
+
+    public void pageValidate(PageEvent event);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/event/package.html b/tapestry-framework/src/org/apache/tapestry/event/package.html
new file mode 100644
index 0000000..70e3206
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/event/package.html
@@ -0,0 +1,28 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+Defines events and listener interfaces for Tapestry.
+
+<p>
+{@link org.apache.tapestry.event.ChangeObserver}
+and
+{@link org.apache.tapestry.event.ObservedChangeEvent}
+are used to communicate changes in persistent properties
+from pages and components to page recorders.
+
+<p>
+The remaining interfaces
+{@link org.apache.tapestry.event.PageDetachListener} and
+{@link org.apache.tapestry.event.PageRenderListener} allow
+objects to know about key lifecycle events regarding
+a page.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/form/AbstractFormComponent.java b/tapestry-framework/src/org/apache/tapestry/form/AbstractFormComponent.java
new file mode 100644
index 0000000..134dfe3
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/AbstractFormComponent.java
@@ -0,0 +1,85 @@
+//  Copyright 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.tapestry.form;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.valid.IValidationDelegate;
+
+/**
+ *  A base class for building components that correspond to HTML form elements.
+ *  All such components must be wrapped (directly or indirectly) by
+ *  a {@link Form} component.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *  @since 1.0.3
+ * 
+ **/
+
+public abstract class AbstractFormComponent extends AbstractComponent implements IFormComponent
+{
+    /**
+     *  Returns the {@link Form} wrapping this component.  Invokes
+     *  {@link #setForm(IForm)} (so that the component may know, later, what the
+     *  form is).  Also, if the form has a delegate, 
+     *  then {@link IValidationDelegate#setFormComponent(IFormComponent)} is invoked.
+     *
+     *  @throws ApplicationRuntimeException if the component is not wrapped by a {@link Form}.
+     *
+     **/
+
+    public IForm getForm(IRequestCycle cycle)
+    {
+        IForm result = Form.get(cycle);
+
+        if (result == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("AbstractFormComponent.must-be-contained-by-form"),
+                this,
+                null,
+                null);
+
+        setForm(result);
+
+        IValidationDelegate delegate = result.getDelegate();
+
+        if (delegate != null)
+            delegate.setFormComponent(this);
+
+        return result;
+    }
+
+    public abstract IForm getForm();
+    public abstract void setForm(IForm form);
+
+    public abstract String getName();
+    public abstract void setName(String name);
+
+    /**
+     *  Implemented in some subclasses to provide a display name (suitable
+     *  for presentation to the user as a label or error message).  This implementation
+     *  return null.
+     * 
+     **/
+
+    public String getDisplayName()
+    {
+        return null;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/AbstractTextField.java b/tapestry-framework/src/org/apache/tapestry/form/AbstractTextField.java
new file mode 100644
index 0000000..37cd8e4
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/AbstractTextField.java
@@ -0,0 +1,127 @@
+//  Copyright 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.tapestry.form;
+
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  Base class for implementing various types of text input fields.
+ *  This includes {@link TextField} and
+ *  {@link org.apache.tapestry.valid.ValidField}.
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.2
+ * 
+ **/
+
+public abstract class AbstractTextField extends AbstractFormComponent
+{
+    /**
+     *  Renders the form element, or responds when the form containing the element
+     *  is submitted (by checking {@link Form#isRewinding()}.
+     *
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        String value;
+
+        IForm form = getForm(cycle);
+
+        // It isn't enough to know whether the cycle in general is rewinding, need to know
+        // specifically if the form which contains this component is rewinding.
+
+        boolean rewinding = form.isRewinding();
+
+        // If the cycle is rewinding, but the form containing this field is not,
+        // then there's no point in doing more work.
+
+        if (!rewinding && cycle.isRewinding())
+            return;
+
+        // Used whether rewinding or not.
+
+        String name = form.getElementId(this);
+		
+        if (rewinding)
+        {
+            if (!isDisabled())
+            {
+                value = cycle.getRequestContext().getParameter(name);
+
+                updateValue(value);
+            }
+
+            return;
+        }
+
+        writer.beginEmpty("input");
+
+        writer.attribute("type", isHidden() ? "password" : "text");
+
+        if (isDisabled())
+            writer.attribute("disabled", "disabled");
+
+        writer.attribute("name", name);
+
+        value = readValue();
+        if (value != null)
+            writer.attribute("value", value);
+
+        renderInformalParameters(writer, cycle);
+
+        beforeCloseTag(writer, cycle);
+
+        writer.closeTag();
+    }
+
+    /**
+     *  Invoked from {@link #render(IMarkupWriter, IRequestCycle)}
+     *  just before the tag is closed.  This implementation does nothing,
+     *  subclasses may override.
+     *
+     **/
+
+    protected void beforeCloseTag(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        // Do nothing.
+    }
+
+    /**
+     *  Invoked by {@link #render(IMarkupWriter writer, IRequestCycle cycle)}
+     *  when a value is obtained from the
+     *  {@link javax.servlet.http.HttpServletRequest}.
+     *
+     **/
+
+    abstract protected void updateValue(String value);
+
+    /**
+     *  Invoked by {@link #render(IMarkupWriter writer, IRequestCycle cycle)}
+     *  when rendering a response.
+     *
+     *  @return the current value for the field, as a String, or null.
+     **/
+
+    abstract protected String readValue();
+
+    public abstract boolean isHidden();
+
+    public abstract boolean isDisabled();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/Button.java b/tapestry-framework/src/org/apache/tapestry/form/Button.java
new file mode 100644
index 0000000..190e6f3
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/Button.java
@@ -0,0 +1,73 @@
+//  Copyright 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.tapestry.form;
+
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  Implements a component that manages an HTML &lt;input type=button&gt; form element.
+ *
+ *  [<a href="../../../../../ComponentReference/Button.html">Component Reference</a>]
+ *  
+ *  <p>This component is useful for attaching JavaScript onclick event handlers.
+ *
+ *  @author Howard Lewis Ship
+ *  @author Paul Geerts
+ *  @author Malcolm Edgar
+ *  @version $Id$
+ **/
+
+public abstract class Button extends AbstractFormComponent
+{
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        IForm form = getForm(cycle);
+
+        boolean rewinding = form.isRewinding();
+
+        String name = form.getElementId(this);
+
+        if (rewinding)
+        {
+            return;
+        }
+
+        writer.beginEmpty("input");
+        writer.attribute("type", "button");
+        writer.attribute("name", name);
+
+        if (isDisabled())
+        {
+            writer.attribute("disabled", "disabled");
+        }
+
+        String label = getLabel();
+
+        if (label != null)
+        {
+            writer.attribute("value", label);
+        }
+
+        renderInformalParameters(writer, cycle);
+
+        writer.closeTag();
+    }
+
+    public abstract String getLabel();
+
+    public abstract boolean isDisabled();
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/form/Button.jwc b/tapestry-framework/src/org/apache/tapestry/form/Button.jwc
new file mode 100644
index 0000000..53b3dad
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/Button.jwc
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.form.Button" allow-body="no">
+
+  <description>
+  Creates a labeled button within a form.
+  </description>
+
+  <parameter name="label" type="java.lang.String" direction="in"/>
+  <parameter name="disabled" type="boolean" direction="in"/>
+  
+  <reserved-parameter name="name"/>
+  <reserved-parameter name="type"/>
+  
+  <property-specification name="name" type="java.lang.String"/>
+  <property-specification name="form" type="org.apache.tapestry.IForm"/>
+
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/form/Checkbox.java b/tapestry-framework/src/org/apache/tapestry/form/Checkbox.java
new file mode 100644
index 0000000..9c47ee5
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/Checkbox.java
@@ -0,0 +1,87 @@
+//  Copyright 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.tapestry.form;
+
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  Implements a component that manages an HTML &lt;input type=checkbox&gt;
+ *  form element.
+ *
+ *  [<a href="../../../../../ComponentReference/Checkbox.html">Component Reference</a>]
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public abstract class Checkbox extends AbstractFormComponent
+{
+    /**
+     *  Renders the form elements, or responds when the form containing the element
+     *  is submitted (by checking {@link Form#isRewinding()}.
+     *
+     *  <p>In traditional HTML, many checkboxes would have the same name but different values.
+     *  Under Tapestry, it makes more sense to have different names and a fixed value.
+     *  For a checkbox, we only care about whether the name appears as a request parameter.
+     *
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        IForm form = getForm(cycle);
+		
+        // Used whether rewinding or not.
+
+        String name = form.getElementId(this);
+
+        if (form.isRewinding())
+        {
+            String value = cycle.getRequestContext().getParameter(name);
+
+            setSelected((value != null));
+
+            return;
+        }
+
+        writer.beginEmpty("input");
+        writer.attribute("type", "checkbox");
+
+        writer.attribute("name", name);
+
+        if (isDisabled())
+            writer.attribute("disabled", "disabled");
+
+        if (isSelected())
+            writer.attribute("checked", "checked");
+
+        renderInformalParameters(writer, cycle);
+
+        writer.closeTag();
+    }
+
+    public abstract boolean isDisabled();
+
+    /** @since 2.2 **/
+
+    public abstract boolean isSelected();
+
+    /** @since 2.2 **/
+
+    public abstract void setSelected(boolean selected);
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/Checkbox.jwc b/tapestry-framework/src/org/apache/tapestry/form/Checkbox.jwc
new file mode 100644
index 0000000..4f1b307
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/Checkbox.jwc
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.form.Checkbox" allow-body="no">
+
+  <description>
+  Implements a checkbox within a Form.
+  </description>
+  
+  <parameter name="selected" 
+  	type="boolean"
+  	required="yes" 
+  	direction="form">
+    <description>
+    The property read and updated by the Checkbox.
+    </description>
+  </parameter>
+  
+  <parameter name="disabled" type="boolean" direction="in">
+    <description>
+    If true, then the checkbox will be disabled and any input from the checkbox
+    will be ignored.
+    </description>
+  </parameter>
+  
+  <reserved-parameter name="type"/>
+  <reserved-parameter name="checked"/>
+  <reserved-parameter name="name"/>
+  
+  <property-specification name="name" type="java.lang.String"/>
+  <property-specification name="form" type="org.apache.tapestry.IForm"/>
+  
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/form/DatePicker.java b/tapestry-framework/src/org/apache/tapestry/form/DatePicker.java
new file mode 100644
index 0000000..dcf1de8
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/DatePicker.java
@@ -0,0 +1,250 @@
+//  Copyright 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.tapestry.form;
+
+import java.text.DateFormatSymbols;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IAsset;
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.IScript;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.engine.IScriptSource;
+import org.apache.tapestry.html.Body;
+
+/**
+ * Provides a Form <tt>java.util.Date</tt> field component for selecting dates.
+ *
+ *  [<a href="../../../../../ComponentReference/DatePicker.html">Component Reference</a>]
+ *
+ * @author Paul Geerts
+ * @author Malcolm Edgar
+ * @version $Id$
+ * @since 2.2
+ * 
+ */
+
+public abstract class DatePicker extends AbstractFormComponent
+{
+    public abstract String getFormat();
+
+    public abstract Date getValue();
+
+    public abstract void setValue(Date value);
+
+    public abstract boolean isDisabled();
+
+    public abstract boolean getIncludeWeek();
+
+    public abstract IAsset getIcon();
+
+    private IScript _script;
+
+    private static final String SYM_NAME = "name";
+    private static final String SYM_FORMNAME = "formName";
+    private static final String SYM_MONTHNAMES = "monthNames";
+    private static final String SYM_SHORT_MONTHNAMES = "shortMonthNames";
+    private static final String SYM_WEEKDAYNAMES = "weekDayNames";
+    private static final String SYM_SHORT_WEEKDAYNAMES = "shortWeekDayNames";
+    private static final String SYM_FIRSTDAYINWEEK = "firstDayInWeek";
+    private static final String SYM_MINDAYSINFIRSTWEEK = "minimalDaysInFirstWeek";
+    private static final String SYM_FORMAT = "format";
+    private static final String SYM_INCL_WEEK = "includeWeek";
+    private static final String SYM_VALUE = "value";
+    private static final String SYM_BUTTONONCLICKHANDLER = "buttonOnclickHandler";
+
+    // Output symbol
+
+    private static final String SYM_BUTTONNAME = "buttonName";
+
+    protected void finishLoad()
+    {
+        IEngine engine = getPage().getEngine();
+        IScriptSource source = engine.getScriptSource();
+
+        IResourceLocation location =
+            getSpecification().getSpecificationLocation().getRelativeLocation("DatePicker.script");
+
+        _script = source.getScript(location);
+    }
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        IForm form = getForm(cycle);
+
+        String name = form.getElementId(this);
+
+        String format = getFormat();
+
+        if (format == null)
+            format = "dd MMM yyyy";
+
+        SimpleDateFormat formatter = new SimpleDateFormat(format, getPage().getLocale());
+
+        boolean disabled = isDisabled();
+
+        if (!cycle.isRewinding())
+        {
+            Body body = Body.get(cycle);
+
+            if (body == null)
+                throw new ApplicationRuntimeException(
+                    Tapestry.format("must-be-contained-by-body", "DatePicker"),
+                    this,
+                    null,
+                    null);
+
+            Locale locale = getPage().getLocale();
+            DateFormatSymbols dfs = new DateFormatSymbols(locale);
+            Calendar cal = Calendar.getInstance(locale);
+
+            Date value = getValue();
+
+            Map symbols = new HashMap();
+
+            symbols.put(SYM_NAME, name);
+            symbols.put(SYM_FORMAT, format);
+            symbols.put(SYM_INCL_WEEK, getIncludeWeek() ? Boolean.TRUE : Boolean.FALSE);
+
+            symbols.put(SYM_MONTHNAMES, makeStringList(dfs.getMonths(), 0, 12));
+            symbols.put(SYM_SHORT_MONTHNAMES, makeStringList(dfs.getShortMonths(), 0, 12));
+            symbols.put(SYM_WEEKDAYNAMES, makeStringList(dfs.getWeekdays(), 1, 8));
+            symbols.put(SYM_SHORT_WEEKDAYNAMES, makeStringList(dfs.getShortWeekdays(), 1, 8));
+            symbols.put(SYM_FIRSTDAYINWEEK, new Integer(cal.getFirstDayOfWeek() - 1));
+            symbols.put(SYM_MINDAYSINFIRSTWEEK, new Integer(cal.getMinimalDaysInFirstWeek()));
+            symbols.put(SYM_FORMNAME, form.getName());
+            symbols.put(SYM_VALUE, value);
+
+            _script.execute(cycle, body, symbols);
+
+            writer.beginEmpty("input");
+            writer.attribute("type", "text");
+            writer.attribute("name", name);
+            writer.attribute("title", formatter.toLocalizedPattern());
+
+            if (value != null)
+                writer.attribute("value", formatter.format(value));
+
+            if (disabled)
+                writer.attribute("disabled", "disabled");
+
+            renderInformalParameters(writer, cycle);
+            
+            writer.printRaw("&nbsp;");
+
+            if (!disabled)
+            {
+                writer.begin("a");
+                writer.attribute("href", (String) symbols.get(SYM_BUTTONONCLICKHANDLER));
+            }
+
+            IAsset icon = getIcon();
+
+            writer.beginEmpty("img");
+            writer.attribute("src", icon.buildURL(cycle));
+            writer.attribute("border", 0);
+
+            if (!disabled)
+                writer.end(); // <a>
+
+        }
+
+        if (form.isRewinding())
+        {
+            if (disabled)
+                return;
+
+            String textValue = cycle.getRequestContext().getParameter(name);
+
+            if (Tapestry.isBlank(textValue))
+                return;
+
+            try
+            {
+                Date value = formatter.parse(textValue);
+
+                setValue(value);
+            }
+            catch (ParseException ex)
+            {
+            }
+        }
+
+    }
+
+    /**
+     * Create a list of quoted strings. The list is suitable for
+     * initializing a JavaScript array.
+     */
+    private String makeStringList(String[] a, int offset, int length)
+    {
+        StringBuffer b = new StringBuffer();
+        for (int i = offset; i < length; i++)
+        {
+            // JavaScript is sensitive to some UNICODE characters. So for
+            // the sake of simplicity, we just escape everything
+            b.append('"');
+            char[] ch = a[i].toCharArray();
+            for (int j = 0; j < ch.length; j++)
+            {
+                if (ch[j] < 128)
+                {
+                    b.append(ch[j]);
+                }
+                else
+                {
+                    b.append(escape(ch[j]));
+                }
+            }
+
+            b.append('"');
+            if (i < length - 1)
+            {
+                b.append(", ");
+            }
+        }
+        return b.toString();
+
+    }
+
+    /**
+     * Create an escaped Unicode character
+     * @param c
+     * @return The unicode character in escaped string form
+     */
+    private static String escape(char c)
+    {
+        StringBuffer b = new StringBuffer();
+        for (int i = 0; i < 4; i++)
+        {
+            b.append(Integer.toHexString(c & 0x000F).toUpperCase());
+            c >>>= 4;
+        }
+        b.append("u\\");
+        return b.reverse().toString();
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/form/DatePicker.js b/tapestry-framework/src/org/apache/tapestry/form/DatePicker.js
new file mode 100644
index 0000000..c74d6ce
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/DatePicker.js
@@ -0,0 +1,992 @@
+//

+// calendar -- a javascript date picker designed for easy localization.

+//

+// $Id$

+//

+//

+// Author: Per Norrman (pernorrman@telia.com)

+// 

+// Based on Tapestry 2.3-beta1 Datepicker by Paul Geerts

+// 

+// Thanks to:

+//     Vladimir [vyc@quorus-ms.ru] for fixing the IE6 zIndex problem.

+//

+// The normal setup would be to have one text field for displaying the 

+// selected date, and one button to show/hide the date picker control.

+// This is  the recommended javascript code:

+// 

+//	<script language="javascript">

+//		var cal;

+//

+//		function init() {

+//			cal = new Calendar();

+//			cal.setIncludeWeek(true);

+//			cal.setFormat("yyyy-MM-dd");

+//			cal.setMonthNames(.....);

+//			cal.setShortMonthNames(....);

+//			cal.create();

+//			

+//			document.form.button1.onclick = function() {

+//				cal.toggle(document.form.button1);

+//			}

+//			cal.onchange = function() {

+//				document.form.textfield1.value  = cal.formatDate();

+//			}

+//		}

+//	</script>

+//

+// The init function is invoked when the body is loaded.

+//

+//

+

+function Calendar(date) {

+	if (arguments.length == 0) {

+		this._currentDate = new Date();

+		this._selectedDate = null;

+	}

+	else {

+		this._currentDate = new Date(date);

+		this._selectedDate = new Date(date);

+	}

+

+	// Accumulated days per month, for normal and for leap years.

+	// Used in week number calculations.	

+    Calendar.NUM_DAYS = [0,31,59,90,120,151,181,212,243,273,304,334];

+    

+    Calendar.LEAP_NUM_DAYS = [0,31,60,91,121,152,182,213,244,274,305,335];

+    

+

+	this._bw = new bw_check();

+	this._showing = false;	

+	this._includeWeek = false;

+	this._hideOnSelect = true;

+	this._alwaysVisible = false;

+	

+	this._dateSlot = new Array(42);

+	this._weekSlot = new Array(6);

+	

+	this._firstDayOfWeek = 1;

+	this._minimalDaysInFirstWeek = 4;

+	

+	this._monthNames = [	

+		"January",		"February",		"March",	"April",

+		"May",			"June",			"July",		"August",

+		"September",	"October",		"November",	"December"

+	];

+	

+	this._shortMonthNames = [ 

+		"jan", "feb", "mar", "apr", "may", "jun", 

+		"jul", "aug", "sep", "oct", "nov", "dec"

+	];

+	

+	// Week days start with Sunday=0, ... Saturday=6

+	this._weekDayNames = [

+		"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" 

+	];

+	

+	this._shortWeekDayNames = 

+		["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ];

+	

+	this._defaultFormat = "yyyy-MM-dd";

+	

+	this._format = this._defaultFormat; 

+

+	this._calDiv = null;

+	

+	

+}

+

+/**

+ *	CREATE the Calendar DOM element

+ */

+Calendar.prototype.create = function() {

+	var div;

+	var table;

+	var tbody;

+	var tr;

+	var td;

+	var dp = this;

+	

+	// Create the top-level div element

+	this._calDiv = document.createElement("div");

+	this._calDiv.className = "calendar";

+	this._calDiv.style.position = "absolute";

+	this._calDiv.style.display = "none";

+	this._calDiv.style.border = "1px solid WindowText";

+	this._calDiv.style.textAlign = "center";

+	this._calDiv.style.background = "Window";

+	this._calDiv.style.zIndex = "400";

+	

+	

+	// header div

+	div = document.createElement("div");

+	div.className = "calendarHeader";

+	div.style.background = "ActiveCaption";

+	div.style.padding = "3px";

+	div.style.borderBottom = "1px solid WindowText";

+	this._calDiv.appendChild(div);

+	

+	table = document.createElement("table");

+	table.style.cellSpacing = 0;

+	div.appendChild(table);

+	

+	tbody = document.createElement("tbody");

+	table.appendChild(tbody);

+	

+	tr = document.createElement("tr");

+	tbody.appendChild(tr);

+	

+	// Previous Month Button

+	td = document.createElement("td");

+	this._previousMonth = document.createElement("button");

+	this._previousMonth.className = "prevMonthButton"

+	this._previousMonth.appendChild(document.createTextNode("<<"));

+	//this._previousMonth.appendChild(document.createTextNode(String.fromCharCode(9668)));

+	td.appendChild(this._previousMonth);

+	tr.appendChild(td);

+	

+	

+	

+	//

+	// Create the month drop down 

+	//

+	td = document.createElement("td");

+	td.className = "labelContainer";

+	tr.appendChild(td);

+	this._monthSelect = document.createElement("select");

+    for (var i = 0 ; i < this._monthNames.length ; i++) {

+        var opt = document.createElement("option");

+        opt.innerHTML = this._monthNames[i];

+        opt.value = i;

+        if (i == this._currentDate.getMonth()) {

+            opt.selected = true;

+        }

+        this._monthSelect.appendChild(opt);

+    }

+	td.appendChild(this._monthSelect);

+	

+

+	// 

+	// Create the year drop down

+	//

+	td = document.createElement("td");

+	td.className = "labelContainer";

+	tr.appendChild(td);

+	this._yearSelect = document.createElement("select");

+	for(var i=1920; i < 2050; ++i) {

+		var opt = document.createElement("option");

+		opt.innerHTML = i;

+		opt.value = i;

+		if (i == this._currentDate.getFullYear()) {

+			opt.selected = false;

+		}

+		this._yearSelect.appendChild(opt);

+	}

+	td.appendChild(this._yearSelect);

+	

+	

+	td = document.createElement("td");

+	this._nextMonth = document.createElement("button");

+	this._nextMonth.appendChild(document.createTextNode(">>"));

+	//this._nextMonth.appendChild(document.createTextNode(String.fromCharCode(9654)));

+	this._nextMonth.className = "nextMonthButton";

+	td.appendChild(this._nextMonth);

+	tr.appendChild(td);

+	

+	// Calendar body

+	div = document.createElement("div");

+	div.className = "calendarBody";

+	this._calDiv.appendChild(div);

+	this._table = div;

+	

+	// Create the inside of calendar body	

+	

+	var text;

+	table = document.createElement("table");

+	//table.style.width="100%";

+	table.className = "grid";

+	table.style.font 	 	= "small-caption";

+	table.style.fontWeight 	= "normal";

+	table.style.textAalign	= "center";

+	table.style.color		= "WindowText";

+	table.style.cursor		= "default";

+	table.cellPadding		= "3";

+	table.cellSpacing		= "0";

+	

+    div.appendChild(table);

+	var thead = document.createElement("thead");

+	table.appendChild(thead);

+	tr = document.createElement("tr");

+	thead.appendChild(tr);

+	

+	// weekdays header

+	if (this._includeWeek) {

+		td = document.createElement("th");

+		text = document.createTextNode("w");

+		td.appendChild(text);

+		td.className = "weekNumberHead";

+		td.style.textAlign = "left";

+		tr.appendChild(td);

+	}

+	for(i=0; i < 7; ++i) {

+		td = document.createElement("th");

+		text = document.createTextNode(this._shortWeekDayNames[(i+this._firstDayOfWeek)%7]);

+		td.appendChild(text);

+		td.className = "weekDayHead";

+		td.style.fontWeight = "bold";

+		td.style.borderBottom = "1px solid WindowText";

+		tr.appendChild(td);

+	}

+	

+	// Date grid

+	tbody = document.createElement("tbody");

+	table.appendChild(tbody);

+	

+	for(week=0; week<6; ++week) {

+		tr = document.createElement("tr");

+		tbody.appendChild(tr);

+

+		if (this._includeWeek) {

+			td = document.createElement("td");

+			td.className = "weekNumber";

+			td.style.fontWeight = "normal";

+			td.style.borderRight = "1px solid WindowText";

+			td.style.textAlign = "left";

+			text = document.createTextNode(String.fromCharCode(160));

+			td.appendChild(text);

+            //setCursor(td);

+            td.align="center";

+			tr.appendChild(td);

+			var tmp = new Object();

+			tmp.tag = "WEEK";

+			tmp.value = -1;

+			tmp.data = text;

+			this._weekSlot[week] = tmp;

+		}

+

+		for(day=0; day<7; ++day) {

+			td = document.createElement("td");

+			text = document.createTextNode(String.fromCharCode(160));

+			td.appendChild(text);

+            setCursor(td);

+            td.align="center";

+            td.style.fontWeight="normal";

+            

+			tr.appendChild(td);

+			var tmp = new Object();

+			tmp.tag = "DATE";

+			tmp.value = -1;

+			tmp.data = text;

+			this._dateSlot[(week*7)+day] = tmp;

+			

+		}

+	}

+	

+	// Calendar Footer

+	div = document.createElement("div");

+	div.className = "calendarFooter";

+	this._calDiv.appendChild(div);

+	

+	table = document.createElement("table");

+	//table.style.width="100%";

+	table.className = "footerTable";

+	table.cellSpacing = 0;

+	div.appendChild(table);

+	

+	tbody = document.createElement("tbody");

+	table.appendChild(tbody);

+	

+	tr = document.createElement("tr");

+	tbody.appendChild(tr);

+

+	//

+	// The TODAY button	

+	//

+	td = document.createElement("td");

+	this._todayButton = document.createElement("button");

+	var today = new Date();

+	var buttonText = today.getDate() + " " + this._monthNames[today.getMonth()] + ", " + today.getFullYear();

+	this._todayButton.appendChild(document.createTextNode(buttonText));

+	td.appendChild(this._todayButton);

+	tr.appendChild(td);

+	

+	//

+	// The CLEAR button

+	//

+	td = document.createElement("td");

+	this._clearButton = document.createElement("button");

+	var today = new Date();

+	buttonText = "Clear";

+	this._clearButton.appendChild(document.createTextNode(buttonText));

+	td.appendChild(this._clearButton);

+	tr.appendChild(td);

+	

+	

+	this._update();

+	this._updateHeader();

+	

+

+

+	// IE55+ extension		

+	this._previousMonth.hideFocus = true;

+	this._nextMonth.hideFocus = true;

+	this._todayButton.hideFocus = true;

+	// end IE55+ extension

+	

+	// hook up events

+	// buttons

+	this._previousMonth.onclick = function () {

+		dp.prevMonth();

+	};

+

+	this._nextMonth.onclick = function () {

+		dp.nextMonth();

+	};

+

+	this._todayButton.onclick = function () {

+		dp.setSelectedDate(new Date());

+		dp.hide();

+	};

+

+	this._clearButton.onclick = function () {

+		dp.clearSelectedDate();

+		dp.hide();

+	};

+	

+

+	this._calDiv.onselectstart = function () {

+		return false;

+	};

+	

+	this._table.onclick = function (e) {

+		// find event

+		if (e == null) e = document.parentWindow.event;

+		

+		// find td

+		var el = e.target != null ? e.target : e.srcElement;

+		while (el.nodeType != 1)

+			el = el.parentNode;

+		while (el != null && el.tagName && el.tagName.toLowerCase() != "td")

+			el = el.parentNode;

+		

+		// if no td found, return

+		if (el == null || el.tagName == null || el.tagName.toLowerCase() != "td")

+			return;

+		

+		var d = new Date(dp._currentDate);

+		var n = Number(el.firstChild.data);

+		if (isNaN(n) || n <= 0 || n == null)

+			return;

+		

+		if (el.className == "weekNumber")

+			return;

+			

+		d.setDate(n);

+		dp.setSelectedDate(d);

+

+		if (!dp._alwaysVisible && dp._hideOnSelect) {

+			dp.hide();

+		}

+		

+	};

+	

+	

+	this._calDiv.onkeydown = function (e) {

+		if (e == null) e = document.parentWindow.event;

+		var kc = e.keyCode != null ? e.keyCode : e.charCode;

+

+		if(kc == 13) {

+			var d = new Date(dp._currentDate).valueOf();

+			dp.setSelectedDate(d);

+

+			if (!dp._alwaysVisible && dp._hideOnSelect) {

+				dp.hide();

+			}

+			return false;

+		}

+			

+		

+		if (kc < 37 || kc > 40) return true;

+		

+		var d = new Date(dp._currentDate).valueOf();

+		if (kc == 37) // left

+			d -= 24 * 60 * 60 * 1000;

+		else if (kc == 39) // right

+			d += 24 * 60 * 60 * 1000;

+		else if (kc == 38) // up

+			d -= 7 * 24 * 60 * 60 * 1000;

+		else if (kc == 40) // down

+			d += 7 * 24 * 60 * 60 * 1000;

+

+		dp.setCurrentDate(new Date(d));

+		return false;

+	}

+	

+	// ie6 extension

+	this._calDiv.onmousewheel = function (e) {

+		if (e == null) e = document.parentWindow.event;

+		var n = - e.wheelDelta / 120;

+		var d = new Date(dp._currentDate);

+		var m = d.getMonth() + n;

+		d.setMonth(m);

+		

+		

+		dp.setCurrentDate(d);

+		

+		return false;

+	}

+

+	this._monthSelect.onchange = function(e) {

+		if (e == null) e = document.parentWindow.event;

+		e = getEventObject(e);

+		dp.setMonth(e.value);

+	}

+

+	this._monthSelect.onclick = function(e) {

+		if (e == null) e = document.parentWindow.event;

+		e = getEventObject(e);

+		e.cancelBubble = true;

+	}

+	

+	this._yearSelect.onchange = function(e) {

+		if (e == null) e = document.parentWindow.event;

+		e = getEventObject(e);

+		dp.setYear(e.value);

+	}

+

+

+	document.body.appendChild(this._calDiv);

+	

+	

+	return this._calDiv;

+}

+

+Calendar.prototype._update = function() {

+

+

+	// Calculate the number of days in the month for the selected date

+	var date = this._currentDate;

+	var today = toISODate(new Date());

+	

+	

+	var selected = "";

+	if (this._selectedDate != null) {

+		selected = toISODate(this._selectedDate);

+	}

+	var current = toISODate(this._currentDate);

+	var d1 = new Date(date.getFullYear(), date.getMonth(), 1);

+	var d2 = new Date(date.getFullYear(), date.getMonth()+1, 1);

+	var monthLength = Math.round((d2 - d1) / (24 * 60 * 60 * 1000));

+	

+	// Find out the weekDay index for the first of this month

+	var firstIndex = (d1.getDay() - this._firstDayOfWeek) % 7 ;

+    if (firstIndex < 0) {

+    	firstIndex += 7;

+    }

+	

+	var index = 0;

+	while (index < firstIndex) {

+		this._dateSlot[index].value = -1;

+		this._dateSlot[index].data.data = String.fromCharCode(160);

+		this._dateSlot[index].data.parentNode.className = "";

+		this._dateSlot[index].data.parentNode.style.fontWeight = "normal";

+		this._dateSlot[index].data.parentNode.style.border= "none";

+		index++;

+	}

+        

+    for (i = 1; i <= monthLength; i++, index++) {

+		this._dateSlot[index].value = i;

+		this._dateSlot[index].data.data = i;

+		this._dateSlot[index].data.parentNode.className = "";

+		this._dateSlot[index].data.parentNode.style.fontWeight = "normal";

+		this._dateSlot[index].data.parentNode.style.border= "none";

+		if (toISODate(d1) == today) {

+			this._dateSlot[index].data.parentNode.className = "today";

+			this._dateSlot[index].data.parentNode.style.fontWeight = "bold";

+		}

+		if (toISODate(d1) == current) {

+			this._dateSlot[index].data.parentNode.className += " current";

+			this._dateSlot[index].data.parentNode.style.border= "1px dotted WindowText";

+		}

+		if (toISODate(d1) == selected) {

+			this._dateSlot[index].data.parentNode.className += " selected";

+			this._dateSlot[index].data.parentNode.style.border= "1px solid WindowText";

+		}

+		d1 = new Date(d1.getFullYear(), d1.getMonth(), d1.getDate()+1);

+	}

+	

+	var lastDateIndex = index;

+        

+    while(index < 42) {

+		this._dateSlot[index].value = -1;

+		this._dateSlot[index].data.data = String.fromCharCode(160);

+		this._dateSlot[index].data.parentNode.className = "";

+		this._dateSlot[index].data.parentNode.style.fontWeight = "normal";

+		this._dateSlot[index].data.parentNode.style.border= "none";

+		++index;

+	}

+	

+	// Week numbers

+	if (this._includeWeek) {

+		d1 = new Date(date.getFullYear(), date.getMonth(), 1);

+		for (i=0; i < 6; ++i) {

+			if (i == 5 && lastDateIndex < 36) {

+				this._weekSlot[i].data.data = String.fromCharCode(160);

+				this._weekSlot[i].data.parentNode.style.borderRight = "none";

+			} else {

+				week = weekNumber(this, d1);

+				this._weekSlot[i].data.data = week;

+				this._weekSlot[i].data.parentNode.style.borderRight = "1px solid WindowText";

+			}

+			d1 = new Date(d1.getFullYear(), d1.getMonth(), d1.getDate()+7);

+		}

+	}

+}

+

+Calendar.prototype.show = function(element) {

+	if(!this._showing) {

+		var p = getPoint(element);

+		this._calDiv.style.display = "block";

+		this._calDiv.style.top = (p.y + element.offsetHeight + 1) + "px";

+		this._calDiv.style.left = p.x + "px";

+		this._showing = true;

+		

+		/* -------- */

+	   	if( this._bw.ie6 )

+	   	{

+	     	dw = this._calDiv.offsetWidth;

+	     	dh = this._calDiv.offsetHeight;

+	     	var els = document.getElementsByTagName("body");

+	     	var body = els[0];

+	     	if( !body ) return;

+	 

+	    	//paste iframe under the modal

+		     var underDiv = this._calDiv.cloneNode(false); 

+		     underDiv.style.zIndex="390";

+		     underDiv.style.margin = "0px";

+		     underDiv.style.padding = "0px";

+		     underDiv.style.display = "block";

+		     underDiv.style.width = dw;

+		     underDiv.style.height = dh;

+		     underDiv.style.border = "1px solid WindowText";

+		     underDiv.innerHTML = "<iframe width=\"100%\" height=\"100%\" frameborder=\"0\"></iframe>";

+		     body.appendChild(underDiv);

+		     this._underDiv = underDiv;

+	   }

+		/* -------- */

+		this._calDiv.focus();

+		

+	}

+};

+

+Calendar.prototype.hide = function() {   

+	if(this._showing) {

+		this._calDiv.style.display = "none";

+		this._showing = false;

+		if( this._bw.ie6 ) {

+		    if( this._underDiv ) this._underDiv.removeNode(true);

+		}

+	}

+}

+

+Calendar.prototype.toggle = function(element) {

+	if(this._showing) {

+		this.hide(); 

+	} else {

+		this.show(element);

+	}

+}

+

+

+

+Calendar.prototype.onchange = function() {};

+

+

+Calendar.prototype.setCurrentDate = function(date) {

+	if (date == null) {

+		return;

+	}

+

+	// if string or number create a Date object

+	if (typeof date == "string" || typeof date == "number") {

+		date = new Date(date);

+	}

+	

+	

+	// do not update if not really changed

+	if (this._currentDate.getDate() != date.getDate() ||

+		this._currentDate.getMonth() != date.getMonth() || 

+		this._currentDate.getFullYear() != date.getFullYear()) {

+		

+		this._currentDate = new Date(date);

+	

+		this._updateHeader();

+		this._update();

+		

+	}

+	

+}

+

+Calendar.prototype.setSelectedDate = function(date) {

+	this._selectedDate = new Date(date);

+	this.setCurrentDate(this._selectedDate);

+	if (typeof this.onchange == "function") {

+		this.onchange();

+	}

+}

+

+Calendar.prototype.clearSelectedDate = function() {

+	this._selectedDate = null;

+	if (typeof this.onchange == "function") {

+		this.onchange();

+	}

+}

+

+Calendar.prototype.getElement = function() {

+	return this._calDiv;

+}

+

+Calendar.prototype.setIncludeWeek = function(v) {

+	if (this._calDiv == null) {

+		this._includeWeek = v;

+	}

+}

+

+Calendar.prototype.getSelectedDate = function () {

+	if (this._selectedDate == null) {

+		return null;

+	} else {

+		return new Date(this._selectedDate);

+	}

+}

+

+

+

+Calendar.prototype._updateHeader = function () {

+

+	// 

+	var options = this._monthSelect.options;

+	var m = this._currentDate.getMonth();

+	for(var i=0; i < options.length; ++i) {

+		options[i].selected = false;

+		if (options[i].value == m) {

+			options[i].selected = true;

+		}

+	}

+	

+	options = this._yearSelect.options;

+	var year = this._currentDate.getFullYear();

+	for(var i=0; i < options.length; ++i) {

+		options[i].selected = false;

+		if (options[i].value == year) {

+			options[i].selected = true;

+		}

+	}

+	

+}

+

+Calendar.prototype.setYear = function(year) {

+	var d = new Date(this._currentDate);

+	d.setFullYear(year);

+	this.setCurrentDate(d);

+}

+

+Calendar.prototype.setMonth = function (month) {

+	var d = new Date(this._currentDate);

+	d.setMonth(month);

+	this.setCurrentDate(d);

+}

+

+Calendar.prototype.nextMonth = function () {

+	this.setMonth(this._currentDate.getMonth()+1);

+}

+

+Calendar.prototype.prevMonth = function () {

+	this.setMonth(this._currentDate.getMonth()-1);

+}

+

+Calendar.prototype.setFirstDayOfWeek = function (nFirstWeekDay) {

+	this._firstDayOfWeek = nFirstWeekDay;

+}

+

+Calendar.prototype.getFirstDayOfWeek = function () {

+	return this._firstDayOfWeek;

+}

+

+Calendar.prototype.setMinimalDaysInFirstWeek = function(n) {

+	this._minimalDaysInFirstWeek = n;

+}

+

+

+Calendar.prototype.getMinimalDaysInFirstWeek = function () {

+	return this._minimalDaysInFirstWeek;

+}

+

+Calendar.prototype.setMonthNames = function(a) {

+	// sanity test

+	this._monthNames = a;

+}

+

+Calendar.prototype.setShortMonthNames = function(a) {

+	// sanity test

+	this._shortMonthNames = a;

+}

+

+Calendar.prototype.setWeekDayNames = function(a) {

+	// sanity test

+	this._weekDayNames = a;

+}

+

+Calendar.prototype.setShortWeekDayNames = function(a) {

+	// sanity test

+	this._shortWeekDayNames = a;

+}

+

+Calendar.prototype.getFormat = function() {

+	return this._format;

+}

+	

+Calendar.prototype.setFormat = function(f) {

+	this._format = f;

+}

+

+Calendar.prototype.formatDate = function() {  

+	if (this._selectedDate == null) {

+		return "";

+	}

+	

+    var bits = new Array();

+    // work out what each bit should be

+    var date = this._selectedDate;

+    bits['d'] = date.getDate();

+    bits['dd'] = pad(date.getDate(),2);

+    bits['ddd'] = this._shortWeekDayNames[date.getDay()];

+    bits['dddd'] = this._weekDayNames[date.getDay()];

+

+    bits['M'] = date.getMonth()+1;

+    bits['MM'] = pad(date.getMonth()+1,2);

+    bits['MMM'] = this._shortMonthNames[date.getMonth()];

+    bits['MMMM'] = this._monthNames[date.getMonth()];

+    

+    var yearStr = "" + date.getFullYear();

+    yearStr = (yearStr.length == 2) ? '19' + yearStr: yearStr;

+    bits['yyyy'] = yearStr;

+    bits['yy'] = bits['yyyy'].toString().substr(2,2);

+

+    // do some funky regexs to replace the format string

+    // with the real values

+    var frm = new String(this._format);

+    var sect;

+    for (sect in bits) {

+      frm = eval("frm.replace(/\\b" + sect + "\\b/,'" + bits[sect] + "');");

+    }

+

+    return frm;

+}

+	

+                                                                                                       

+function isLeapYear(year) {

+	return ((year%4 == 0) && ((year%100 != 0) || (year%400 == 0)));

+}

+

+function yearLength(year) {

+	if (isLeapYear(year))

+		return 366;

+	else

+		return 365;

+}

+

+function dayOfYear(date) {

+	var a = Calendar.NUM_DAYS;

+	if (isLeapYear(date.getFullYear())) {

+		a = Calendar.LEAP_NUM_DAYS;

+	}

+	var month = date.getMonth();

+	

+	return a[month] + date.getDate();

+}

+

+// ---------------------------------------------

+// Week number stuff

+// ---------------------------------------------

+

+function weekNumber(cal, date) {

+

+	var dow = date.getDay();

+	var doy = dayOfYear(date);

+	var year = date.getFullYear();

+

+	// Compute the week of the year.  Valid week numbers run from 1 to 52

+	// or 53, depending on the year, the first day of the week, and the

+	// minimal days in the first week.  Days at the start of the year may

+	// fall into the last week of the previous year; days at the end of

+	// the year may fall into the first week of the next year.

+	var relDow = (dow + 7 - cal.getFirstDayOfWeek()) % 7; // 0..6

+	var relDowJan1 = (dow - doy + 701 - cal.getFirstDayOfWeek()) % 7; // 0..6

+	var week = Math.floor((doy - 1 + relDowJan1) / 7); // 0..53

+	if ((7 - relDowJan1) >= cal.getMinimalDaysInFirstWeek()) {

+		++week;

+	}

+

+	if (doy > 359) { // Fast check which eliminates most cases

+		// Check to see if we are in the last week; if so, we need

+		// to handle the case in which we are the first week of the

+		// next year.

+		var lastDoy = yearLength(year);

+		var lastRelDow = (relDow + lastDoy - doy) % 7;

+		if (lastRelDow < 0) {

+			lastRelDow += 7;

+		}

+		if (((6 - lastRelDow) >= cal.getMinimalDaysInFirstWeek())

+			&& ((doy + 7 - relDow) > lastDoy)) {

+			week = 1;

+		}

+	} else if (week == 0) {

+		// We are the last week of the previous year.

+		var prevDoy = doy + yearLength(year - 1);

+		week = weekOfPeriod(cal, prevDoy, dow);

+	}

+

+	return week;

+}

+

+function weekOfPeriod(cal, dayOfPeriod, dayOfWeek) {

+	// Determine the day of the week of the first day of the period

+	// in question (either a year or a month).  Zero represents the

+	// first day of the week on this calendar.

+	var periodStartDayOfWeek =

+		(dayOfWeek - cal.getFirstDayOfWeek() - dayOfPeriod + 1) % 7;

+	if (periodStartDayOfWeek < 0) {

+		periodStartDayOfWeek += 7;

+	}

+

+	// Compute the week number.  Initially, ignore the first week, which

+	// may be fractional (or may not be).  We add periodStartDayOfWeek in

+	// order to fill out the first week, if it is fractional.

+	var weekNo = Math.floor((dayOfPeriod + periodStartDayOfWeek - 1) / 7);

+

+	// If the first week is long enough, then count it.  If

+	// the minimal days in the first week is one, or if the period start

+	// is zero, we always increment weekNo.

+	if ((7 - periodStartDayOfWeek) >= cal.getMinimalDaysInFirstWeek()) {

+		++weekNo;

+	}

+

+	return weekNo;

+}

+

+

+

+

+function getEventObject(e) {  // utility function to retrieve object from event

+    if (navigator.appName == "Microsoft Internet Explorer") {

+        return e.srcElement;

+    } else {  // is mozilla/netscape

+        // need to crawl up the tree to get the first "real" element

+        // i.e. a tag, not raw text

+        var o = e.target;

+        while (!o.tagName) {

+            o = o.parentNode;

+        }

+        return o;

+    }

+}

+

+function addEvent(name, obj, funct) { // utility function to add event handlers

+

+    if (navigator.appName == "Microsoft Internet Explorer") {

+        obj.attachEvent("on"+name, funct);

+    } else {  // is mozilla/netscape

+        obj.addEventListener(name, funct, false);

+    }

+}

+

+

+function deleteEvent(name, obj, funct) { // utility function to delete event handlers

+

+    if (navigator.appName == "Microsoft Internet Explorer") {

+        obj.detachEvent("on"+name, funct);

+    } else {  // is mozilla/netscape

+        obj.removeEventListener(name, funct, false);

+    }

+}

+

+function setCursor(obj) {

+   if (navigator.appName == "Microsoft Internet Explorer") {

+        obj.style.cursor = "hand";

+    } else {  // is mozilla/netscape

+        obj.style.cursor = "pointer";

+    }

+}

+

+function Point(iX, iY)

+{

+   this.x = iX;

+   this.y = iY;

+}

+

+

+function getPoint(aTag)

+{

+   var oTmp = aTag;  

+   var point = new Point(0,0);

+  

+   do 

+   {

+      point.x += oTmp.offsetLeft;

+      point.y += oTmp.offsetTop;

+      oTmp = oTmp.offsetParent;

+   } 

+   while (oTmp.tagName != "BODY");

+

+   return point;

+}

+

+function toISODate(date) {

+	var s = date.getFullYear();

+	var m = date.getMonth() + 1;

+	if (m < 10) {

+		m = "0" + m;

+	}

+	var day = date.getDate();

+	if (day < 10) {

+		day = "0" + day;

+	}

+	return String(s) + String(m) + String(day);

+	

+}

+

+function pad(number,X) {   // utility function to pad a number to a given width

+	X = (!X ? 2 : X);

+	number = ""+number;

+	while (number.length < X) {

+	    number = "0" + number;

+	}

+	return number;

+}

+

+function bw_check()

+{

+    var is_major = parseInt( navigator.appVersion );

+    this.nver = is_major;

+    this.ver = navigator.appVersion;

+    this.agent = navigator.userAgent;

+    this.dom = document.getElementById ? 1 : 0;

+    this.opera = window.opera ? 1 : 0;

+    this.ie5 = ( this.ver.indexOf( "MSIE 5" ) > -1 && this.dom && !this.opera ) ? 1 : 0;

+    this.ie6 = ( this.ver.indexOf( "MSIE 6" ) > -1 && this.dom && !this.opera ) ? 1 : 0;

+    this.ie4 = ( document.all && !this.dom && !this.opera ) ? 1 : 0;

+    this.ie = this.ie4 || this.ie5 || this.ie6;

+    this.mac = this.agent.indexOf( "Mac" ) > -1;

+    this.ns6 = ( this.dom && parseInt( this.ver ) >= 5 ) ? 1 : 0;

+    this.ie3 = ( this.ver.indexOf( "MSIE" ) && ( is_major < 4 ) );

+    this.hotjava = ( this.agent.toLowerCase().indexOf( 'hotjava' ) != -1 ) ? 1 : 0;

+    this.ns4 = ( document.layers && !this.dom && !this.hotjava ) ? 1 : 0;

+    this.bw = ( this.ie6 || this.ie5 || this.ie4 || this.ns4 || this.ns6 || this.opera );

+    this.ver3 = ( this.hotjava || this.ie3 );

+    this.opera7 = ( ( this.agent.toLowerCase().indexOf( 'opera 7' ) > -1 ) || ( this.agent.toLowerCase().indexOf( 'opera/7' ) > -1 ) );

+    this.operaOld = this.opera && !this.opera7;

+    return this;

+};

+

+

diff --git a/tapestry-framework/src/org/apache/tapestry/form/DatePicker.jwc b/tapestry-framework/src/org/apache/tapestry/form/DatePicker.jwc
new file mode 100644
index 0000000..72cff6f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/DatePicker.jwc
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification
+	class="org.apache.tapestry.form.DatePicker"
+	allow-informal-parameters="yes" 
+	allow-body="no">
+
+  <parameter name="value" direction="form" type="java.util.Date" required="yes"/>
+  <parameter name="format" direction="in" type="java.lang.String" required="no"/>
+  <parameter name="disabled" direction="in" type="boolean" required="no"/>
+  <parameter name="includeWeek" type="boolean" direction="in" required="no"/>
+  <parameter name="icon" type="org.apache.tapestry.IAsset" direction="in" required="no"
+  	  default-value="assets.defaultIcon"/>
+        
+  <property-specification name="name" type="java.lang.String"/>
+  <property-specification name="form" type="org.apache.tapestry.IForm"/>
+  
+  <private-asset name="script" resource-path="DatePicker.js"/>
+  
+  <private-asset name="defaultIcon" resource-path="DatePickerIcon.png"/>
+                
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/form/DatePicker.script b/tapestry-framework/src/org/apache/tapestry/form/DatePicker.script
new file mode 100644
index 0000000..b1ca083
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/DatePicker.script
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- $Id$ -->
+<!DOCTYPE script PUBLIC
+	"-//Apache Software Foundation//Tapestry Script Specification 3.0//EN"
+	"http://jakarta.apache.org/tapestry/dtd/Script_3_0.dtd">
+  
+<script>
+
+<include-script resource-path="DatePicker.js"/>
+
+<input-symbol key="name"  class="java.lang.String" required="yes"/>
+<input-symbol key="formName" class="java.lang.String" required="yes"/>
+<input-symbol key="monthNames"  required="yes"/>
+<input-symbol key="shortMonthNames" required="yes"/>
+<input-symbol key="weekDayNames" required="yes"/>
+<input-symbol key="shortWeekDayNames"  required="yes"/>
+<input-symbol key="firstDayInWeek" required="yes"/>
+<input-symbol key="minimalDaysInFirstWeek" required="yes"/>
+<input-symbol key="format" required="yes"/>
+<input-symbol key="includeWeek" required="yes"/>
+<input-symbol key="value" required="no"/>
+
+<let key="calendarObject" unique="yes">
+	calendar_${name}	
+</let>
+
+<let key="buttonOnclickHandler">
+  javascript:${calendarObject}.toggle(document.${formName}.${name});	
+</let>
+
+<body>
+var ${calendarObject};
+</body>
+
+<initialization>
+	
+<if expression="value == null">
+${calendarObject} = new Calendar();
+</if>
+<if expression="value != null">
+${calendarObject} = new Calendar(${value.time});
+</if>
+		
+${calendarObject}.setMonthNames(new Array(${monthNames}));
+${calendarObject}.setShortMonthNames(new Array(${shortMonthNames}));
+${calendarObject}.setWeekDayNames(new Array(${weekDayNames}));
+${calendarObject}.setShortWeekDayNames(new Array(${shortWeekDayNames}));
+${calendarObject}.setFormat("${format}");
+${calendarObject}.setFirstDayOfWeek(${firstDayInWeek});
+${calendarObject}.setMinimalDaysInFirstWeek(${minimalDaysInFirstWeek});
+${calendarObject}.setIncludeWeek(${includeWeek});
+
+${calendarObject}.create();
+
+${calendarObject}.onchange = function() {
+  var field = document.${formName}.${name};
+  var value = ${calendarObject}.formatDate();
+  if (field.value != value) {
+    field.value = value;
+    if (field.onchange) {
+      field.onchange();
+    }
+  }
+}
+
+</initialization>
+</script>
diff --git a/tapestry-framework/src/org/apache/tapestry/form/DatePickerIcon.png b/tapestry-framework/src/org/apache/tapestry/form/DatePickerIcon.png
new file mode 100644
index 0000000..0b9a96e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/DatePickerIcon.png
Binary files differ
diff --git a/tapestry-framework/src/org/apache/tapestry/form/EnumPropertySelectionModel.java b/tapestry-framework/src/org/apache/tapestry/form/EnumPropertySelectionModel.java
new file mode 100644
index 0000000..0ffdfad
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/EnumPropertySelectionModel.java
@@ -0,0 +1,141 @@
+//  Copyright 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.tapestry.form;
+
+import java.util.ResourceBundle;
+
+import org.apache.commons.lang.enum.Enum;
+
+/**
+ *  Implementation of {@link IPropertySelectionModel} that wraps around
+ *  a set of {@link Enum}s.
+ * 
+ *  <p>Uses a simple index number as the value (used to represent the option).
+ *
+ *  <p>The resource bundle from which labels are extracted is usually
+ *  a resource within the Tapestry application.  Since 
+ *  {@link ResourceBundle#getBundle(String, java.util.Locale)} uses its caller's class loader,
+ *  and that classloader will be the Tapestry framework's classloader, the application's
+ *  resources won't be visible.  This requires that the application resolve
+ *  the resource to a {@link ResourceBundle} before creating this model.
+ *  
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ * 
+ **/
+
+public class EnumPropertySelectionModel implements IPropertySelectionModel
+{
+    private Enum[] _options;
+    private String[] _labels;
+
+    private String _resourcePrefix;
+    private ResourceBundle _bundle;
+
+    /**
+     *  Standard constructor.
+     *
+     *  <p>Labels for the options are extracted from a resource bundle.  resourceBaseName
+     *  identifies the bundle.  Typically, the bundle will be a <code>.properties</code>
+     *  file within the classpath.  Specify the fully qualified class name equivalent, i.e.,
+     *  for file <code>/com/example/foo/LabelStrings.properties</code> use
+     *  <code>com.example.foo.LabelStrings</code> as the resource base name.
+     *
+     *  <p>Normally (when resourcePrefix is null), the keys used to extract labels
+     *  matches the {@link Enum#getName() enumeration id} of the option.  By
+     *  convention, the enumeration id matches the name of the static variable.
+     *
+     *  <p>To avoid naming conflicts when using a single resource bundle for multiple
+     *  models, use a resource prefix.  This is a string which is prepended to
+     *  the enumeration id (they prefix and enumeration id are seperated with a period).
+     *
+     *  @param   options The list of possible values for this model, in the order they
+     *  should appear. This exact array is retained (not copied).
+     *
+     *  @param   bundle The {@link ResourceBundle} from which labels may be extracted.
+     *
+     *  @param   resourcePrefix An optional prefix used when accessing keys within the bundle. 
+     *  Used to allow a single ResouceBundle to contain labels for multiple Enums.
+     **/
+
+    public EnumPropertySelectionModel(Enum[] options, ResourceBundle bundle, String resourcePrefix)
+    {
+        _options = options;
+        _bundle = bundle;
+        _resourcePrefix = resourcePrefix;
+    }
+
+    /**
+     *  Simplified constructor using no prefix.
+     *
+     **/
+
+    public EnumPropertySelectionModel(Enum[] options, ResourceBundle bundle)
+    {
+        this(options, bundle, null);
+    }
+
+    public int getOptionCount()
+    {
+        return _options.length;
+    }
+
+    public Object getOption(int index)
+    {
+        return _options[index];
+    }
+
+    public String getLabel(int index)
+    {
+        if (_labels == null)
+            readLabels();
+
+        return _labels[index];
+    }
+
+    public String getValue(int index)
+    {
+        return Integer.toString(index);
+    }
+
+    public Object translateValue(String value)
+    {
+        int index;
+
+        index = Integer.parseInt(value);
+
+        return _options[index];
+    }
+
+    private void readLabels()
+    {
+        _labels = new String[_options.length];
+
+        for (int i = 0; i < _options.length; i++)
+        {
+            String enumerationId = _options[i].getName();
+
+            String key;
+
+            if (_resourcePrefix == null)
+                key = enumerationId;
+            else
+                key = _resourcePrefix + "." + enumerationId;
+
+            _labels[i] = _bundle.getString(key);
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/Form.java b/tapestry-framework/src/org/apache/tapestry/form/Form.java
new file mode 100644
index 0000000..e0e3175
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/Form.java
@@ -0,0 +1,828 @@
+//  Copyright 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.tapestry.form;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IActionListener;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IDirect;
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.RenderRewoundException;
+import org.apache.tapestry.StaleLinkException;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.engine.IEngineService;
+import org.apache.tapestry.engine.ILink;
+import org.apache.tapestry.html.Body;
+import org.apache.tapestry.util.IdAllocator;
+import org.apache.tapestry.util.StringSplitter;
+import org.apache.tapestry.valid.IValidationDelegate;
+
+/**
+ *  Component which contains form element components.  Forms use the
+ *  action or direct services to handle the form submission.  A Form will wrap
+ *  other components and static HTML, including
+ *  form components such as {@link TextArea}, {@link TextField}, {@link Checkbox}, etc.
+ * 
+ *  [<a href="../../../../../ComponentReference/Form.html">Component Reference</a>]
+ * 
+ *  <p>When a form is submitted, it continues through the rewind cycle until
+ *  <em>after</em> all of its wrapped elements have renderred.  As the form
+ *  component render (in the rewind cycle), they will be updating
+ *  properties of the containing page and notifying thier listeners.  Again:
+ *  each form component is responsible not only for rendering HTML (to present the
+ *  form), but for handling it's share of the form submission.
+ *
+ *  <p>Only after all that is done will the Form notify its listener.
+ *
+ *  <p>Starting in release 1.0.2, a Form can use either the direct service or
+ *  the action service.  The default is the direct service, even though
+ *  in earlier releases, only the action service was available.
+ *
+ *  @author Howard Lewis Ship, David Solis
+ *  @version $Id$
+ **/
+
+public abstract class Form extends AbstractComponent implements IForm, IDirect
+{
+    private static class HiddenValue
+    {
+        String _name;
+        String _value;
+        String _id;
+
+        private HiddenValue(String name, String value)
+        {
+			this(name, null, value);
+        }
+
+		private HiddenValue(String name, String id, String value)
+		{
+			_name	= name;
+			_id		= id;
+			_value	= value;
+		}
+    }
+
+    private boolean _rewinding;
+    private boolean _rendering;
+    private String _name;
+
+    /**
+     *  Used when rewinding the form to figure to match allocated ids (allocated during
+     *  the rewind) against expected ids (allocated in the previous request cycle, when
+     *  the form was rendered).
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    private int _allocatedIdIndex;
+
+    /**
+     *  The list of allocated ids for form elements within this form.  This list
+     *  is constructed when a form renders, and is validated against when the
+     *  form is rewound.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    private List _allocatedIds = new ArrayList();
+
+    /**
+     *  {@link Map}, keyed on {@link FormEventType}.  Values are either a String (the name
+     *  of a single event), or a {@link List} of Strings.
+     *
+     *  @since 1.0.2
+     **/
+
+    private Map _events;
+
+    private static final int EVENT_MAP_SIZE = 3;
+
+    private IdAllocator _elementIdAllocator = new IdAllocator();
+
+    private String _encodingType;
+
+    private List _hiddenValues;
+
+    /**
+     *  Returns the currently active {@link IForm}, or null if no form is
+     *  active.  This is a convienience method, the result will be
+     *  null, or an instance of {@link IForm}, but not necessarily a
+     *  <code>Form</code>.
+     *
+     **/
+
+    public static IForm get(IRequestCycle cycle)
+    {
+        return (IForm) cycle.getAttribute(ATTRIBUTE_NAME);
+    }
+
+    /**
+     *  Indicates to any wrapped form components that they should respond to the form
+     *  submission.
+     *
+     *  @throws ApplicationRuntimeException if not rendering.
+     **/
+
+    public boolean isRewinding()
+    {
+        if (!_rendering)
+            throw Tapestry.createRenderOnlyPropertyException(this, "rewinding");
+
+        return _rewinding;
+    }
+
+    /**
+     *  Returns true if this Form is configured to use the direct
+     *  service.
+     *
+     *  <p>This is derived from the direct parameter, and defaults
+     *  to true if not bound.
+     *
+     *  @since 1.0.2
+     **/
+
+    public abstract boolean isDirect();
+
+    /**
+     *  Returns true if the stateful parameter is bound to
+     *  a true value.  If stateful is not bound, also returns
+     *  the default, true.
+     *
+     *  @since 1.0.1
+     **/
+
+    public boolean getRequiresSession()
+    {
+        return isStateful();
+    }
+
+    /**
+     *  Constructs a unique identifier (within the Form).  The identifier
+     *  consists of the component's id, with an index number added to
+     *  ensure uniqueness.
+     *
+     *  <p>Simply invokes {@link #getElementId(org.apache.tapestry.form.IFormComponent, java.lang.String)}
+     *  with the component's id.
+     *
+     *
+     *  @since 1.0.2
+     **/
+
+    public String getElementId(IFormComponent component)
+    {
+        return getElementId(component, component.getId());
+    }
+
+    /**
+     *  Constructs a unique identifier from the base id.  If possible, the
+     *  id is used as-is.  Otherwise, a unique identifier is appended
+     *  to the id.
+     *
+     *  <p>This method is provided simply so that some components
+     * ({@link ImageSubmit}) have more specific control over
+     *  their names.
+     *
+     *  @since 1.0.3
+     *
+     **/
+
+    public String getElementId(IFormComponent component, String baseId)
+    {
+        String result = _elementIdAllocator.allocateId(baseId);
+
+        if (_rewinding)
+        {
+            if (_allocatedIdIndex >= _allocatedIds.size())
+            {
+                throw new StaleLinkException(
+                    Tapestry.format(
+                        "Form.too-many-ids",
+                        getExtendedId(),
+                        Integer.toString(_allocatedIds.size()),
+                        component.getExtendedId()),
+                    this);
+            }
+
+            String expected = (String) _allocatedIds.get(_allocatedIdIndex);
+
+            if (!result.equals(expected))
+                throw new StaleLinkException(
+                    Tapestry.format(
+                        "Form.id-mismatch",
+                        new Object[] {
+                            getExtendedId(),
+                            Integer.toString(_allocatedIdIndex + 1),
+                            expected,
+                            result,
+                            component.getExtendedId()}),
+                    this);
+        }
+        else
+        {
+            _allocatedIds.add(result);
+        }
+
+        _allocatedIdIndex++;
+
+        component.setName(result);
+
+        return result;
+    }
+
+    /**
+     *  Returns the name generated for the form.  This is used to faciliate
+     *  components that write JavaScript and need to access the form or
+     *  its contents.
+     *
+     *  <p>This value is generated when the form renders, and is not cleared.
+     *  If the Form is inside a {@link org.apache.tapestry.components.Foreach}, 
+     *  this will be the most recently
+     *  generated name for the Form.
+     *
+     *  <p>This property is exposed so that sophisticated applications can write
+     *  JavaScript handlers for the form and components within the form.
+     *
+     *  @see AbstractFormComponent#getName()
+     *
+     **/
+
+    public String getName()
+    {
+        return _name;
+    }
+
+    /** @since 3.0 **/
+
+    protected void prepareForRender(IRequestCycle cycle)
+    {
+        super.prepareForRender(cycle);
+
+        if (cycle.getAttribute(ATTRIBUTE_NAME) != null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("Form.forms-may-not-nest"),
+                this,
+                null,
+                null);
+
+        cycle.setAttribute(ATTRIBUTE_NAME, this);
+    }
+
+    protected void cleanupAfterRender(IRequestCycle cycle)
+    {
+        _rendering = false;
+
+        _allocatedIdIndex = 0;
+        _allocatedIds.clear();
+
+        _events = null;
+
+        _elementIdAllocator.clear();
+
+        if (_hiddenValues != null)
+            _hiddenValues.clear();
+
+        cycle.removeAttribute(ATTRIBUTE_NAME);
+
+        _encodingType = null;
+
+        IValidationDelegate delegate = getDelegate();
+
+        if (delegate != null)
+            delegate.setFormComponent(null);
+
+        super.cleanupAfterRender(cycle);
+    }
+
+    protected void writeAttributes(IMarkupWriter writer, ILink link)
+    {
+        String method = getMethod();
+
+        writer.begin(getTag());
+        writer.attribute("method", (method == null) ? "post" : method);
+        writer.attribute("name", _name);
+        writer.attribute("action", link.getURL(null, false));
+
+        if (_encodingType != null)
+            writer.attribute("enctype", _encodingType);
+    }
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        String actionId = cycle.getNextActionId();
+        _name = getDisplayName() + actionId;
+
+        boolean renderForm = !cycle.isRewinding();
+        boolean rewound = cycle.isRewound(this);
+
+        _rewinding = rewound;
+
+        _allocatedIdIndex = 0;
+
+        _rendering = true;
+
+        if (rewound)
+        {
+            String storedIdList = cycle.getRequestContext().getParameter(_name);
+
+            reconstructAllocatedIds(storedIdList);
+        }
+
+        ILink link = getLink(cycle, actionId);
+
+        // When rendering, use a nested writer so that an embedded Upload
+        // component can force the encoding type.
+
+        IMarkupWriter nested = writer.getNestedWriter();
+
+        renderBody(nested, cycle);
+
+        if (renderForm)
+        {
+            writeAttributes(writer, link);
+
+            renderInformalParameters(writer, cycle);
+            writer.println();
+        }
+
+        // Write the hidden's, or at least, reserve the query parameters
+        // required by the Gesture.
+
+        writeLinkParameters(writer, link, !renderForm);
+
+        if (renderForm)
+        {
+            // What's this for?  It's part of checking for stale links.  
+            // We record the list of allocated ids.
+            // On rewind, we check that the stored list against which
+            // ids were allocated.  If the persistent state of the page or
+            // application changed between render (previous request cycle)
+            // and rewind (current request cycle), then the list
+            // of ids will change as well.
+
+            writeHiddenField(writer, _name, buildAllocatedIdList());
+            writeHiddenValues(writer);
+
+            nested.close();
+
+            writer.end(getTag());
+
+            // Write out event handlers collected during the rendering.
+
+            emitEventHandlers(writer, cycle);
+        }
+
+        if (rewound)
+        {
+            int expected = _allocatedIds.size();
+
+            // The other case, _allocatedIdIndex > expected, is
+            // checked for inside getElementId().  Remember that
+            // _allocatedIdIndex is incremented after allocating.
+
+            if (_allocatedIdIndex < expected)
+            {
+                String nextExpectedId = (String) _allocatedIds.get(_allocatedIdIndex);
+
+                throw new StaleLinkException(
+                    Tapestry.format(
+                        "Form.too-few-ids",
+                        getExtendedId(),
+                        Integer.toString(expected - _allocatedIdIndex),
+                        nextExpectedId),
+                    this);
+            }
+
+            IActionListener listener = getListener();
+
+            if (listener != null)
+                listener.actionTriggered(this, cycle);
+
+            // Abort the rewind render.
+
+            throw new RenderRewoundException(this);
+        }
+    }
+
+    /**
+     *  Adds an additional event handler.
+     *
+     *  @since 1.0.2
+     * 
+     **/
+
+    public void addEventHandler(FormEventType type, String functionName)
+    {
+        if (_events == null)
+            _events = new HashMap(EVENT_MAP_SIZE);
+
+        Object value = _events.get(type);
+
+        // The value can either be a String, or a List of String.  Since
+        // it is rare for there to be more than one event handling function,
+        // we start with just a String.
+
+        if (value == null)
+        {
+            _events.put(type, functionName);
+            return;
+        }
+
+        // The second function added converts it to a List.
+
+        if (value instanceof String)
+        {
+            List list = new ArrayList();
+            list.add(value);
+            list.add(functionName);
+
+            _events.put(type, list);
+            return;
+        }
+
+        // The third and subsequent function just
+        // adds to the List.
+
+        List list = (List) value;
+        list.add(functionName);
+    }
+
+    protected void emitEventHandlers(IMarkupWriter writer, IRequestCycle cycle)
+    {
+
+        if (_events == null || _events.isEmpty())
+            return;
+
+        Body body = Body.get(cycle);
+
+        if (body == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("Form.needs-body-for-event-handlers"),
+                this,
+                null,
+                null);
+
+        StringBuffer buffer = new StringBuffer();
+
+        Iterator i = _events.entrySet().iterator();
+        while (i.hasNext())
+        {
+
+            Map.Entry entry = (Map.Entry) i.next();
+            FormEventType type = (FormEventType) entry.getKey();
+            Object value = entry.getValue();
+
+            buffer.append("document.");
+            buffer.append(_name);
+            buffer.append(".");
+            buffer.append(type.getPropertyName());
+            buffer.append(" = ");
+
+            // The typical case; one event one event handler.  Easy enough.
+
+            if (value instanceof String)
+            {
+                buffer.append(value.toString());
+                buffer.append(";");
+            }
+            else
+            {
+                // Build a composite function in-place
+
+                buffer.append("function ()\n{\n");
+
+                boolean combineWithAnd = type.getCombineUsingAnd();
+
+                List l = (List) value;
+                int count = l.size();
+
+                for (int j = 0; j < count; j++)
+                {
+                    String functionName = (String) l.get(j);
+
+                    if (j > 0)
+                    {
+
+                        if (combineWithAnd)
+                            buffer.append(" &&");
+                        else
+                            buffer.append(";");
+                    }
+
+                    buffer.append("\n  ");
+
+                    if (combineWithAnd)
+                    {
+                        if (j == 0)
+                            buffer.append("return ");
+                        else
+                            buffer.append("  ");
+                    }
+
+                    buffer.append(functionName);
+                    buffer.append("()");
+                }
+
+                buffer.append(";\n}");
+            }
+
+            buffer.append("\n\n");
+        }
+
+        body.addInitializationScript(buffer.toString());
+    }
+
+    /**
+     *  Simply invokes {@link #render(IMarkupWriter, IRequestCycle)}.
+     *
+     *  @since 1.0.2
+     * 
+     **/
+
+    public void rewind(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        render(writer, cycle);
+    }
+
+    /**
+     *  Method invoked by the direct service.
+     *
+     *  @since 1.0.2
+     *
+     **/
+
+    public void trigger(IRequestCycle cycle)
+    {
+        Object[] parameters = cycle.getServiceParameters();
+
+        cycle.rewindForm(this, (String) parameters[0]);
+    }
+
+    /**
+     *  Builds the EngineServiceLink for the form, using either the direct or
+     *  action service. 
+     *
+     *  @since 1.0.3
+     *
+     **/
+
+    private ILink getLink(IRequestCycle cycle, String actionId)
+    {
+        String serviceName = null;
+
+        if (isDirect())
+            serviceName = Tapestry.DIRECT_SERVICE;
+        else
+            serviceName = Tapestry.ACTION_SERVICE;
+
+        IEngine engine = cycle.getEngine();
+        IEngineService service = engine.getService(serviceName);
+
+        // A single service parameter is used to store the actionId.
+
+        return service.getLink(cycle, this, new String[] { actionId });
+    }
+
+    private void writeLinkParameters(IMarkupWriter writer, ILink link, boolean reserveOnly)
+    {
+        String[] names = link.getParameterNames();
+        int count = Tapestry.size(names);
+
+        for (int i = 0; i < count; i++)
+        {
+            String name = names[i];
+
+            // Reserve the name.
+
+            _elementIdAllocator.allocateId(name);
+
+            if (!reserveOnly)
+                writeHiddenFieldsForParameter(writer, link, name);
+        }
+    }
+
+    /**
+     *  @since 3.0
+     *
+     **/
+
+    protected void writeHiddenField(IMarkupWriter writer, String name, String value)
+    {
+		writeHiddenField(writer, name, null, value);
+    }
+
+	protected void writeHiddenField(IMarkupWriter writer, String name, String id, String value)
+	{
+		writer.beginEmpty("input");
+		writer.attribute("type", "hidden");
+		writer.attribute("name", name);
+
+		if(id != null && id.length() != 0)
+			writer.attribute("id", id);
+		
+		writer.attribute("value", value);
+		writer.println();
+	}
+
+    /**
+     *  @since 2.2
+     * 
+     **/
+
+    private void writeHiddenFieldsForParameter(
+        IMarkupWriter writer,
+        ILink link,
+        String parameterName)
+    {
+        String[] values = link.getParameterValues(parameterName);
+
+        for (int i = 0; i < values.length; i++)
+        {
+            writeHiddenField(writer, parameterName, values[i]);
+        }
+    }
+
+    /**
+     *  Converts the allocateIds property into a string, a comma-separated list of ids.
+     *  This is included as a hidden field in the form and is used to identify
+     *  discrepencies when the form is submitted.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    protected String buildAllocatedIdList()
+    {
+        StringBuffer buffer = new StringBuffer();
+        int count = _allocatedIds.size();
+
+        for (int i = 0; i < count; i++)
+        {
+            if (i > 0)
+                buffer.append(',');
+
+            buffer.append(_allocatedIds.get(i));
+        }
+
+        return buffer.toString();
+    }
+
+    /**
+     *  Converts a string passed as a parameter (and containing a comma
+     *  separated list of ids) back into the allocateIds property.
+     * 
+     *  @see #buildAllocatedIdList()
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    protected void reconstructAllocatedIds(String storedIdList)
+    {
+        if (Tapestry.isBlank(storedIdList))
+            return;
+
+        StringSplitter splitter = new StringSplitter(',');
+
+        String[] ids = splitter.splitToArray(storedIdList);
+
+        for (int i = 0; i < ids.length; i++)
+            _allocatedIds.add(ids[i]);
+    }
+
+    public abstract IValidationDelegate getDelegate();
+
+    public abstract void setDelegate(IValidationDelegate delegate);
+
+    public abstract void setDirect(boolean direct);
+
+    public abstract IActionListener getListener();
+
+    public abstract String getMethod();
+
+    /**
+     *  Invoked when not rendering, so it uses the stateful binding.
+     *  If not bound, returns true.
+     * 
+     **/
+
+    public boolean isStateful()
+    {
+        IBinding statefulBinding = getStatefulBinding();
+
+        if (statefulBinding == null)
+            return true;
+
+        return statefulBinding.getBoolean();
+    }
+
+    public abstract IBinding getStatefulBinding();
+
+    protected void finishLoad()
+    {
+        setDirect(true);
+    }
+
+    public void setEncodingType(String encodingType)
+    {
+        if (_encodingType != null && !_encodingType.equals(encodingType))
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "Form.encoding-type-contention",
+                    getExtendedId(),
+                    _encodingType,
+                    encodingType),
+                this,
+                null,
+                null);
+
+        _encodingType = encodingType;
+    }
+
+    /**
+     *  Returns the tag of the form.
+     *
+     *  @since 3.0
+     *
+     **/
+
+    protected String getTag()
+    {
+        return "form";
+    }
+
+    /**
+     * Returns the name of the element.
+     *
+     *
+     *  @since 3.0
+     **/
+
+    protected String getDisplayName()
+    {
+        return "Form";
+    }
+
+    /** @since 3.0 */
+
+    public void addHiddenValue(String name, String value)
+    {
+		if (_hiddenValues == null)
+			_hiddenValues = new ArrayList();
+
+		_hiddenValues.add(new HiddenValue(name, value));
+    }
+
+	/** @since 3.0 */
+
+	public void addHiddenValue(String name, String id, String value)
+	{
+		if (_hiddenValues == null)
+			_hiddenValues = new ArrayList();
+
+		_hiddenValues.add(new HiddenValue(name, id, value));
+	}
+
+    /** 
+     * Writes hidden values accumulated during the render
+     * (by components invoking {@link #addHiddenValue(String, String)}.
+     * 
+     * @since 3.0
+     */
+
+    protected void writeHiddenValues(IMarkupWriter writer)
+    {
+        int count = Tapestry.size(_hiddenValues);
+
+        for (int i = 0; i < count; i++)
+        {
+            HiddenValue hv = (HiddenValue) _hiddenValues.get(i);
+
+            writeHiddenField(writer, hv._name, hv._id, hv._value);
+        }
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/Form.jwc b/tapestry-framework/src/org/apache/tapestry/form/Form.jwc
new file mode 100644
index 0000000..1d4c2c8
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/Form.jwc
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.form.Form">
+
+  <description>
+  Used to implement an HTML form.
+  </description>
+
+  <parameter name="method" type="java.lang.String" direction="in">
+    <description>
+    The method used by the form when it is submitted, defaults to POST.
+    </description>
+  </parameter>
+  
+  <parameter name="listener" 
+  	type="org.apache.tapestry.IActionListener" 
+  	required="no"
+  	direction="in">
+    <description>
+  	Object invoked when the form is submitted, after all form components have responded
+  	to the submission.
+    </description>
+  </parameter>
+  
+  <parameter name="stateful" 
+  	type="boolean" 
+  	direction="custom">
+    <description>
+    If true (the default), then an active HttpSession is required.
+    </description>
+  </parameter>
+  
+  <parameter name="direct" type="boolean" direction="in">
+    <description>
+    If true (the default), then the more efficient direct service is used.
+    If false, then the action service is used.
+    </description>
+  </parameter>
+  
+  <parameter name="delegate" 
+  	type="org.apache.tapestry.valid.IValidationDelegate" direction="in">
+    <description>
+    Specifies the delegate to be used by fields to track input errors.
+    </description>
+  </parameter>
+
+  <reserved-parameter name="action"/>
+
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/form/FormEventType.java b/tapestry-framework/src/org/apache/tapestry/form/FormEventType.java
new file mode 100644
index 0000000..80b1f9d
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/FormEventType.java
@@ -0,0 +1,82 @@
+//  Copyright 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.tapestry.form;
+
+import org.apache.commons.lang.enum.Enum;
+
+/**
+ *  Lists different types of JavaScript events that can be associated
+ *  with a {@link Form} via {@link Form#addEventHandler(FormEventType, String)}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.2
+ **/
+
+public class FormEventType extends Enum
+{
+    /**
+     *  Form event triggered when the form is submitted.  Allows an event handler
+     *  to perform any final changes before the results are posted to the server.
+     *
+     *  <p>The JavaScript method should return <code>true</code> or
+     * <code>false</code>.  If there are multiple event handlers for the form
+     * they will be combined using the binary and operator (<code>&amp;&amp;</code>).
+     *
+     **/
+
+    public static final FormEventType SUBMIT = new FormEventType("SUBMIT", "onsubmit");
+
+    /**
+     *  Form event triggered when the form is reset; this allows an event handler
+     *  to deal with any special cases related to resetting.
+     *
+     **/
+
+    public static final FormEventType RESET = new FormEventType("RESET", "onreset");
+
+    private String _propertyName;
+
+    private FormEventType(String name, String propertyName)
+    {
+        super(name);
+
+        _propertyName = propertyName;
+    }
+
+    /** 
+     *  Returns the DOM property corresponding to event type (used when generating
+     *  client-side scripting).
+     *
+     **/
+
+    public String getPropertyName()
+    {
+        return _propertyName;
+    }
+
+    /**
+     *  Returns true if multiple functions should be combined
+     *  with the <code>&amp;&amp;</code> operator.  Otherwise,
+     *  the event handler functions are simply invoked
+     *  sequentially (as a series of JavaScript statements).
+     *
+     **/
+
+    public boolean getCombineUsingAnd()
+    {
+        return this == FormEventType.SUBMIT;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/Hidden.java b/tapestry-framework/src/org/apache/tapestry/form/Hidden.java
new file mode 100644
index 0000000..7643de1
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/Hidden.java
@@ -0,0 +1,176 @@
+//  Copyright 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.tapestry.form;
+
+import java.io.IOException;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IActionListener;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.util.io.DataSqueezer;
+
+/**
+ *  Implements a hidden field within a {@link Form}.
+ *
+ *  [<a href="../../../../../ComponentReference/Hidden.html">Component Reference</a>]
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public abstract class Hidden extends AbstractFormComponent
+{
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        IForm form = getForm(cycle);
+        boolean formRewound = form.isRewinding();
+
+        String name = form.getElementId(this);
+
+        // If the form containing the Hidden isn't rewound, then render.
+
+        if (!formRewound)
+        {
+            // Optimiziation: if the page is rewinding (some other action or
+            // form was submitted), then don't bother rendering.
+
+            if (cycle.isRewinding())
+                return;
+
+            String externalValue = null;
+
+            if (getEncode())
+            {
+                Object value = getValueBinding().getObject();
+
+                try
+                {
+                    externalValue = getDataSqueezer().squeeze(value);
+                }
+                catch (IOException ex)
+                {
+                    throw new ApplicationRuntimeException(ex.getMessage(), this, null, ex);
+                }
+            }
+            else
+                externalValue = (String) getValueBinding().getObject("value", String.class);
+
+            String id = getElementId();
+            //if we would like to test the IForm.addHiddenValue(name, externalValue) method with
+            //Hidden JUnit test the following code must be default. But from the performance issue
+            //I don't use the id parameter clauses.  
+/*			if(id == null || id.length() == 0){
+				form.addHiddenValue(name, externalValue);
+            }else{
+				form.addHiddenValue(name, id, externalValue);
+            }
+*/
+			form.addHiddenValue(name, id, externalValue);
+            
+
+            return;
+        }
+
+        String externalValue = cycle.getRequestContext().getParameter(name);
+        Object value = null;
+
+        if (getEncode())
+        {
+            try
+            {
+                value = getDataSqueezer().unsqueeze(externalValue);
+            }
+            catch (IOException ex)
+            {
+                throw new ApplicationRuntimeException(ex.getMessage(), this, null, ex);
+            }
+        }
+        else
+            value = externalValue;
+
+        // A listener is not always necessary ... it's easy to code
+        // the synchronization as a side-effect of the accessor method.
+
+        getValueBinding().setObject(value);
+
+        IActionListener listener = getListener();
+
+        if (listener != null)
+            listener.actionTriggered(this, cycle);
+    }
+
+	public String getElementId(){
+		String value = null;
+		IBinding idBinding = getIdBinding();
+		if(idBinding != null){
+			value = idBinding.getString();
+		}
+		return value;
+	}
+
+    /** @since 2.2 **/
+
+    private DataSqueezer getDataSqueezer()
+    {
+        return getPage().getEngine().getDataSqueezer();
+    }
+
+    public abstract IActionListener getListener();
+
+    public abstract IBinding getValueBinding();
+	public abstract IBinding getIdBinding();
+
+    /**
+     * 
+     *  Returns false.  Hidden components are never disabled.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public boolean isDisabled()
+    {
+        return false;
+    }
+
+    /** 
+     * 
+     *  Returns true if the compent encodes object values using a
+     *  {@link org.apache.tapestry.util.io.DataSqueezer}, false
+     *  if values are always Strings.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public abstract boolean getEncode();
+
+    public abstract void setEncode(boolean encode);
+
+    /**
+     * Sets the encode parameter property to its default, true.
+     * 
+     * @since 3.0
+     */
+    protected void finishLoad()
+    {
+        setEncode(true);
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/Hidden.jwc b/tapestry-framework/src/org/apache/tapestry/form/Hidden.jwc
new file mode 100644
index 0000000..faec149
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/Hidden.jwc
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.form.Hidden" 
+	allow-body="no" 
+	allow-informal-parameters="no">
+
+  <description>
+  Stores a value in a hidden field of the form.
+  </description>
+  
+  <parameter name="value" 
+  	required="yes" 
+  	type="java.lang.Object"
+  	direction="custom">
+    <description>
+    Value to save in the form.
+    </description>
+  </parameter>
+  
+  <parameter name="listener" 
+  	type="org.apache.tapestry.IActionListener"
+  	direction="in">
+    <description>
+    Listener notified after the value is restored.
+    </description>
+  </parameter>
+  
+  <parameter name="id" 
+  	required="no" 
+  	type="java.lang.String"
+  	direction="custom">
+    <description>
+    ID parameter of HTML hidden object.
+    </description>
+  </parameter>
+
+  <parameter name="encode" type="boolean" direction="in"/>
+
+  <property-specification name="name" type="java.lang.String"/>
+  <property-specification name="form" type="org.apache.tapestry.IForm"/>
+   
+</component-specification>
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/IFormComponent.java b/tapestry-framework/src/org/apache/tapestry/form/IFormComponent.java
new file mode 100644
index 0000000..5951813
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/IFormComponent.java
@@ -0,0 +1,87 @@
+//  Copyright 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.tapestry.form;
+
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IForm;
+
+/**
+ *  A common interface implemented by all form components (components that
+ *  create interactive elements in the rendered page).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ **/
+
+public interface IFormComponent extends IComponent
+{
+    /**
+     *  Returns the {@link org.apache.tapestry.IForm} which contains the component,
+     *  or null if the component is not contained by a form,
+     *  of if the containing Form is not currently renderring.
+     * 
+     **/
+
+    public IForm getForm();
+
+    /**
+     *  Returns the name of the component, which is automatically generated
+     *  during renderring.
+     *
+     *  <p>This value is set inside the component's render method and is
+     *  <em>not</em> cleared.  If the component is inside a {@link org.apache.tapestry.components.Foreach}, the
+     *  value returned is the most recent name generated for the component.
+     *
+     *  <p>This property is made available to facilitate writing JavaScript that
+     *  allows components (in the client web browser) to interact.
+     *
+     *  <p>In practice, a {@link org.apache.tapestry.html.Script} component
+     *  works with the {@link org.apache.tapestry.html.Body} component to get the
+     *  JavaScript code inserted and referenced.
+     *
+     **/
+
+    public String getName();
+    
+    /**
+     *  Invoked by {@link IForm#getElementId(IFormComponent)} when a name is created
+     *  for a form component.
+     * 
+     *  @since 3.0
+     * 
+     **/
+    
+    public void setName(String name);
+
+    /**
+     *  May be implemented to return a user-presentable, localized name for the component,
+     *  which is used in labels or error messages.  Most components simply return null.
+     * 
+     *  @since 1.0.9
+     * 
+     **/
+
+    public String getDisplayName();
+    
+    /**
+     *  Returns true if the component is disabled.  This is important when the containing
+     *  form is submitted, since disabled parameters do not update their bindings.
+     * 
+     *  @since 2.2
+     * 
+     **/
+    
+    public boolean isDisabled();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/IPropertySelectionModel.java b/tapestry-framework/src/org/apache/tapestry/form/IPropertySelectionModel.java
new file mode 100644
index 0000000..ba31ec6
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/IPropertySelectionModel.java
@@ -0,0 +1,85 @@
+//  Copyright 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.tapestry.form;
+
+/**
+ *  Used by a {@link PropertySelection} to provide labels for options.
+ *
+ *  <p>The component requires three different representations
+ *  of each option:
+ *  <ul>
+ *  <li>The option value, a Java object that will eventually be assigned to
+ *  a property
+ *  <li>The label, a String which is incorprated into the HTML to identify the
+ *  option to the user
+ *  <li>The value, a String which is used to represent the option as the value
+ *  of the &lt;option&gt; or &lt;input type=radio&gt; generated by the
+ *  {@link PropertySelection}.
+ *  </ul>
+ *
+ *  <p>The option is usually either an {@link org.apache.commons.lang.enum.Enum} 
+ *  (see {@link EnumPropertySelectionModel})
+ *  or some kind of business object.  The label is often a property of the
+ *  option object (for example, for a list of customers, it could be the customer name).
+ *
+ *  <p>It should be easy to convert between the value and the option.  It may simply
+ *  be an index into an array.  For business objects, it is often the primary key
+ *  of the object, expressed as a String.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public interface IPropertySelectionModel
+{
+    /**
+     *  Returns the number of possible options.
+     *
+     **/
+
+    public int getOptionCount();
+
+    /**
+     *  Returns one possible option.
+     *
+     **/
+
+    public Object getOption(int index);
+
+    /**
+     *  Returns the label for an option.  It is the responsibility of the
+     *  adaptor to make this value localized.
+     *
+     **/
+
+    public String getLabel(int index);
+
+    /**
+     *  Returns a String used to represent the option in the HTML (as the
+     *  value of an &lt;option&gt; or &lt;input type=radio&gt;.
+     *
+     **/
+
+    public String getValue(int index);
+
+    /**
+     *  Returns the option corresponding to a value.  This is used when
+     *  interpreting submitted form parameters.
+     *
+     **/
+
+    public Object translateValue(String value);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/IPropertySelectionRenderer.java b/tapestry-framework/src/org/apache/tapestry/form/IPropertySelectionRenderer.java
new file mode 100644
index 0000000..137cf6c
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/IPropertySelectionRenderer.java
@@ -0,0 +1,58 @@
+//  Copyright 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.tapestry.form;
+
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  Defines an object that works with a {@link PropertySelection} component
+ *  to render the individual elements obtained from the {@link IPropertySelectionModel model}.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public interface IPropertySelectionRenderer
+{
+    /**
+     *  Begins the rendering of the {@link PropertySelection}.
+     *
+     **/
+
+    public void beginRender(PropertySelection component, IMarkupWriter writer, IRequestCycle cycle);
+
+    /**
+     *  Invoked for each element obtained from the {@link IPropertySelectionModel model}.
+     *
+     **/
+
+    public void renderOption(
+        PropertySelection component,
+        IMarkupWriter writer,
+        IRequestCycle cycle,
+        IPropertySelectionModel model,
+        Object option,
+        int index,
+        boolean selected);
+
+    /**
+     *  Ends the rendering of the {@link PropertySelection}.
+     *
+     **/
+
+    public void endRender(PropertySelection component, IMarkupWriter writer, IRequestCycle cycle);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/ImageSubmit.java b/tapestry-framework/src/org/apache/tapestry/form/ImageSubmit.java
new file mode 100644
index 0000000..611ae2f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/ImageSubmit.java
@@ -0,0 +1,163 @@
+//  Copyright 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.tapestry.form;
+
+import java.awt.Point;
+
+import org.apache.tapestry.IActionListener;
+import org.apache.tapestry.IAsset;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.request.RequestContext;
+
+/**
+ *  Used to create an image button inside a {@link Form}.  Although it
+ *  is occasionally useful to know the {@link Point} on the image that was clicked
+ *  (i.e., use the image as a kind of image map, which was the original intent
+ *  of the HTML element), it is more commonly used to provide a graphic
+ *  image for the user to click, rather than the rather plain &lt;input type=submit&gt;.
+ *
+ *  [<a href="../../../../../ComponentReference/ImageSubmit.html">Component Reference</a>]
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ **/
+
+public abstract class ImageSubmit extends AbstractFormComponent
+{
+
+    public abstract IBinding getPointBinding();
+
+    public abstract IBinding getSelectedBinding();
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        IForm form = getForm(cycle);
+
+        boolean rewinding = form.isRewinding();
+
+        String nameOverride = getNameOverride();
+
+        String name =
+            nameOverride == null ? form.getElementId(this) : form.getElementId(this, nameOverride);
+
+        if (rewinding)
+        {
+            // If disabled, do nothing.
+
+            if (isDisabled())
+                return;
+
+            RequestContext context = cycle.getRequestContext();
+
+            // Image clicks get submitted as two request parameters: 
+            // foo.x and foo.y
+
+            String parameterName = name + ".x";
+
+            String value = context.getParameter(parameterName);
+
+            if (value == null)
+                return;
+
+            // The point parameter is not really used, unless the
+            // ImageButton is used for its original purpose (as a kind
+            // of image map).  In modern usage, we only care about
+            // whether the user clicked on the image (and thus submitted
+            // the form), not where in the image the user actually clicked.
+
+            IBinding pointBinding = getPointBinding();
+
+            if (pointBinding != null)
+            {
+                int x = Integer.parseInt(value);
+
+                parameterName = name + ".y";
+                value = context.getParameter(parameterName);
+
+                int y = Integer.parseInt(value);
+
+                pointBinding.setObject(new Point(x, y));
+            }
+
+            // Notify the application, by setting the select parameter
+            // to the tag parameter.
+
+            IBinding selectedBinding = getSelectedBinding();
+
+            if (selectedBinding != null)
+                selectedBinding.setObject(getTag());
+
+            IActionListener listener = getListener();
+
+            if (listener != null)
+                listener.actionTriggered(this, cycle);
+
+            return;
+        }
+
+        // Not rewinding, do the real render
+
+        boolean disabled = isDisabled();
+        IAsset disabledImage = getDisabledImage();
+
+        IAsset finalImage = (disabled && disabledImage != null) ? disabledImage : getImage();
+
+        String imageURL = finalImage.buildURL(cycle);
+
+        writer.beginEmpty("input");
+        writer.attribute("type", "image");
+        writer.attribute("name", name);
+
+        if (disabled)
+            writer.attribute("disabled", "disabled");
+
+        // NN4 places a border unless you tell it otherwise.
+        // IE ignores the border attribute and never shows a border.
+
+        writer.attribute("border", 0);
+
+        writer.attribute("src", imageURL);
+
+        renderInformalParameters(writer, cycle);
+
+        writer.closeTag();
+    }
+
+    public abstract boolean isDisabled();
+
+    public abstract IAsset getDisabledImage();
+
+    public abstract IAsset getImage();
+
+    public abstract IActionListener getListener();
+
+    public abstract Object getTag();
+
+    public abstract String getNameOverride();
+
+    protected void prepareForRender(IRequestCycle cycle)
+    {
+        super.prepareForRender(cycle);
+
+        if (getImage() == null)
+            throw Tapestry.createRequiredParameterException(this, "image");
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/ImageSubmit.jwc b/tapestry-framework/src/org/apache/tapestry/form/ImageSubmit.jwc
new file mode 100644
index 0000000..df5c8de
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/ImageSubmit.jwc
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.form.ImageSubmit" allow-body="no">
+
+  <description>
+  Creates a clickable image within a form.
+  </description>
+
+  <parameter name="image" 
+  	type="org.apache.tapestry.IAsset" 
+  	required="yes"
+  	direction="in">
+  	<description>
+  	Image used for the button.
+  	</description>
+  </parameter>
+  	
+  <parameter name="name" 
+    property-name="nameOverride"
+  	type="java.lang.String"
+  	direction="in"/>
+  	
+  <parameter name="disabledImage" 
+  	type="org.apache.tapestry.IAsset"
+  	direction="in">
+  	<description>
+  	Image used for the button, if disabled.
+  	</description>
+  </parameter>
+  	
+  <parameter name="disabled"
+  	type="boolean"
+  	direction="in"/>
+  	
+  <parameter name="point" type="java.awt.Point"/>
+  
+  <parameter name="selected"
+  	type="java.lang.Object">
+  	<description>
+  	Property updated when the button is clicked.
+  	</description>
+  </parameter>
+  
+  <parameter name="tag" 
+  	type="java.lang.Object" 
+  	direction="in">
+  	<description>
+  	Value used when updating the selected parameter.
+  	</description>
+  </parameter>
+  
+  <parameter name="listener" 
+  	type="org.apache.tapestry.IActionListener"
+  	direction="in">
+  	<description>
+  	Notified when the button is clicked.
+  	</description>
+  </parameter>
+  
+  <reserved-parameter name="type"/>
+  <reserved-parameter name="src"/>
+  <reserved-parameter name="border"/>
+
+  <property-specification name="name" type="java.lang.String"/>
+  <property-specification name="form" type="org.apache.tapestry.IForm"/>
+ 
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/form/LinkSubmit.java b/tapestry-framework/src/org/apache/tapestry/form/LinkSubmit.java
new file mode 100644
index 0000000..bd5860b
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/LinkSubmit.java
@@ -0,0 +1,181 @@
+//  Copyright 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.tapestry.form;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IActionListener;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.html.Body;
+
+/**
+ *  Implements a component that submits its enclosing form via a JavaScript link.
+ * 
+ *  [<a href="../../../../../ComponentReference/LinkSubmit.html">Component Reference</a>]
+ *
+ *  @author Richard Lewis-Shell
+ *  @version $Id: Submit.java,v 1.6 2003/04/21 13:15:41 glongman Exp $
+ * 
+ **/
+
+public abstract class LinkSubmit extends AbstractFormComponent
+{
+    /**
+     *  The name of an {@link org.apache.tapestry.IRequestCycle} attribute in which the
+     *  current submit link is stored.  LinkSubmits do not nest.
+     *
+     **/
+
+    public static final String ATTRIBUTE_NAME = "org.apache.tapestry.form.LinkSubmit";
+
+    /**
+     * The name of an  {@link org.apache.tapestry.IRequestCycle} attribute in which the
+     * link submit component that generates the javascript function is stored.  The
+     * function is only required once per page (containing a form with a non-disabled
+     * LinkSubmit)
+     * 
+     **/
+    public static final String ATTRIBUTE_FUNCTION_NAME =
+        "org.apache.tapestry.form.LinkSubmit_function";
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+
+        IForm form = getForm(cycle);
+        String formName = form.getName();
+
+        boolean rewinding = form.isRewinding();
+
+        String name = form.getElementId(this);
+
+        IMarkupWriter wrappedWriter;
+
+        if (cycle.getAttribute(ATTRIBUTE_NAME) != null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("LinkSubmit.may-not-nest"),
+                this,
+                null,
+                null);
+
+        cycle.setAttribute(ATTRIBUTE_NAME, this);
+
+        boolean disabled = isDisabled();
+        if (!disabled)
+        {
+            if (!rewinding)
+            {
+                Body body = Body.get(cycle);
+
+				if (body == null)
+				    throw new ApplicationRuntimeException(
+				        Tapestry.format("must-be-contained-by-body", "LinkSubmit"),
+				        this,
+				        null,
+				        null);
+				        				
+                // make sure the submit function is on the page (once)
+                if (cycle.getAttribute(ATTRIBUTE_FUNCTION_NAME) == null)
+                {
+                    body.addBodyScript(
+                        "function submitLink(form, elementId) { form._linkSubmit.value = elementId; if (form.onsubmit == null || form.onsubmit()) form.submit(); }");
+                    cycle.setAttribute(ATTRIBUTE_FUNCTION_NAME, this);
+                }
+
+                // one hidden field per form:
+                String formHiddenFieldAttributeName = ATTRIBUTE_FUNCTION_NAME + formName;
+                if (cycle.getAttribute(formHiddenFieldAttributeName) == null)
+                {
+                	body.addInitializationScript("document." + formName + "._linkSubmit.value = null;"); 
+                    writer.beginEmpty("input");
+                    writer.attribute("type", "hidden");
+                    writer.attribute("name", "_linkSubmit");
+                    cycle.setAttribute(formHiddenFieldAttributeName, this);
+                }
+            }
+            else
+            {
+                // How to know which Submit link was actually
+                // clicked?  When submitted, it sets its elementId into a hidden field
+
+                String value = cycle.getRequestContext().getParameter("_linkSubmit");
+
+                // If the value isn't the elementId of this component, then this link wasn't
+                // selected.
+
+                if (value != null && value.equals(name))
+                {
+                    IBinding selectedBinding = getSelectedBinding();
+                    if (selectedBinding != null)
+                        selectedBinding.setObject(getTag());
+                    IActionListener listener = getListener();
+                    if (listener != null)
+                        listener.actionTriggered(this, cycle);
+                }
+            }
+
+            writer.begin("a");
+            writer.attribute(
+                "href",
+                "javascript:submitLink(document." + formName + ",\"" + name + "\");");
+
+            // Allow the wrapped components a chance to render.
+            // Along the way, they may interact with this component
+            // and cause the name variable to get set.
+
+            wrappedWriter = writer.getNestedWriter();
+        }
+        else
+            wrappedWriter = writer;
+
+        renderBody(wrappedWriter, cycle);
+
+        if (!disabled)
+        {
+            // Generate additional attributes from informal parameters.
+
+            renderInformalParameters(writer, cycle);
+
+            // Dump in HTML provided by wrapped components
+
+            wrappedWriter.close();
+
+            // Close the <a> tag
+
+            writer.end();
+        }
+
+        cycle.removeAttribute(ATTRIBUTE_NAME);
+    }
+
+    public abstract boolean isDisabled();
+
+    public abstract void setDisabled(boolean disabled);
+
+    public abstract IActionListener getListener();
+
+    public abstract void setListener(IActionListener listener);
+
+    public abstract Object getTag();
+
+    public abstract void setTag(Object tag);
+
+    public abstract void setSelectedBinding(IBinding value);
+
+    public abstract IBinding getSelectedBinding();
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/form/LinkSubmit.jwc b/tapestry-framework/src/org/apache/tapestry/form/LinkSubmit.jwc
new file mode 100644
index 0000000..f41452b
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/LinkSubmit.jwc
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<!--

+   Copyright 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.

+-->

+<!-- $Id: Submit.jwc,v 1.2 2003/03/17 18:10:48 hlship Exp $ -->

+<!DOCTYPE component-specification PUBLIC 

+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 

+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">

+

+<component-specification class="org.apache.tapestry.form.LinkSubmit" allow-body="yes">

+

+  <description>

+  Creates a hyperlink that submits its enclosing form using JavaScript.

+  </description>

+

+  <parameter name="listener" 

+  	type="org.apache.tapestry.IActionListener" 

+  	direction="in">

+  	<description>

+	    The listener is invoked during the rewind as the link component is encountered.  This is

+	    both attractive and dangerous when combined with a form.  When the listener is

+	    invoked, the form has not completely rewound, so not all form values have necessarily

+	    been processed, so the listener might be performing its logic based on inconsistent

+	    data.

+  	</description>

+  </parameter>

+  	

+  <parameter name="disabled" type="boolean" direction="in"/>

+  <parameter name="selected"/>

+  <parameter name="tag" type="java.lang.Object" direction="in"/>

+

+  <reserved-parameter name="name"/>

+  <reserved-parameter name="href"/>

+

+  <property-specification name="name" type="java.lang.String"/>

+  <property-specification name="form" type="org.apache.tapestry.IForm"/>  

+  

+</component-specification>
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/ListEdit.java b/tapestry-framework/src/org/apache/tapestry/form/ListEdit.java
new file mode 100644
index 0000000..2222752
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/ListEdit.java
@@ -0,0 +1,184 @@
+//  Copyright 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.tapestry.form;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IActionListener;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.request.RequestContext;
+import org.apache.tapestry.util.io.DataSqueezer;
+
+/**
+ *  A specialized component used to edit a list of items
+ *  within a form; it is similar to a {@link org.apache.tapestry.components.Foreach} but leverages
+ *  hidden inputs within the &lt;form&gt; to store the items in the list.
+ *
+ *  [<a href="../../../../../ComponentReference/ListEdit.html">Component Reference</a>]
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.2
+ * 
+ **/
+
+public abstract class ListEdit extends AbstractFormComponent
+{
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        Iterator i = null;
+
+        IForm form = getForm(cycle);
+
+        boolean cycleRewinding = cycle.isRewinding();
+
+        // If the cycle is rewinding, but not this particular form,
+        // then do nothing (don't even render the body).
+
+        if (cycleRewinding && !form.isRewinding())
+            return;
+
+        String name = form.getElementId(this);
+
+        if (!cycleRewinding)
+        {
+            i = Tapestry.coerceToIterator(getSourceBinding().getObject());
+        }
+        else
+        {
+            RequestContext context = cycle.getRequestContext();
+            String[] submittedValues = context.getParameters(name);
+
+            i = Tapestry.coerceToIterator(submittedValues);
+        }
+
+        // If the source (when rendering), or the submitted values (on submit)
+        // are null, then skip the remainder (nothing to update, nothing to
+        // render).
+
+        if (i == null)
+            return;
+
+        int index = 0;
+
+        IBinding indexBinding = getIndexBinding();
+        IBinding valueBinding = getValueBinding();
+        IActionListener listener = getListener();
+        String element = getElement();
+
+        while (i.hasNext())
+        {
+            Object value = null;
+
+            if (indexBinding != null)
+                indexBinding.setInt(index++);
+
+            if (cycleRewinding)
+                value = convertValue((String) i.next());
+            else
+            {
+                value = i.next();
+                writeValue(form, name, value);
+            }
+
+            valueBinding.setObject(value);
+
+            if (listener != null)
+                listener.actionTriggered(this, cycle);
+
+            if (element != null)
+            {
+                writer.begin(element);
+                renderInformalParameters(writer, cycle);
+            }
+
+            renderBody(writer, cycle);
+
+            if (element != null)
+                writer.end();
+
+        }
+    }
+
+    private void writeValue(IForm form, String name, Object value)
+    {
+        String externalValue;
+
+        try
+        {
+            externalValue = getDataSqueezer().squeeze(value);
+        }
+        catch (IOException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("ListEdit.unable-to-convert-value", value),
+                this,
+                null,
+                ex);
+        }
+
+        form.addHiddenValue(name, externalValue);
+    }
+
+    private Object convertValue(String value)
+    {
+        try
+        {
+            return getDataSqueezer().unsqueeze(value);
+        }
+        catch (IOException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("ListEdit.unable-to-convert-string", value),
+                this,
+                null,
+                ex);
+        }
+    }
+
+    public abstract String getElement();
+
+    private DataSqueezer getDataSqueezer()
+    {
+        return getPage().getEngine().getDataSqueezer();
+    }
+
+    /** @since 2.2 **/
+
+    public abstract IActionListener getListener();
+
+    /** @since 3.0 **/
+
+    public abstract IBinding getSourceBinding();
+
+    public abstract IBinding getValueBinding();
+
+    public abstract IBinding getIndexBinding();
+
+    /** @since 3.0 **/
+
+    public boolean isDisabled()
+    {
+        return false;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/ListEdit.jwc b/tapestry-framework/src/org/apache/tapestry/form/ListEdit.jwc
new file mode 100644
index 0000000..4a03c43
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/ListEdit.jwc
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification allow-body="yes" allow-informal-parameters="yes"
+	class="org.apache.tapestry.form.ListEdit">
+	
+  <description>
+  A looping component, like Foreach, which works well in a form
+  because it stores each element as a hidden field.
+  </description>
+	
+  <parameter name="source" required="yes" type="java.lang.Object"/>
+  
+  <parameter name="listener" type="org.apache.tapestry.IActionListener" direction="in"/>
+  
+  <parameter name="value" required="yes" type="java.lang.Object"/>
+  
+  <parameter name="index" type="int"/>
+  
+  <parameter name="element" type="java.lang.String" direction="in"/>
+
+  <property-specification name="name" type="java.lang.String"/>
+  <property-specification name="form" type="org.apache.tapestry.IForm"/>
+  
+</component-specification>
+
+
diff --git a/tapestry-framework/src/org/apache/tapestry/form/ListEditMap.java b/tapestry-framework/src/org/apache/tapestry/form/ListEditMap.java
new file mode 100644
index 0000000..8befa71
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/ListEditMap.java
@@ -0,0 +1,271 @@
+//  Copyright 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.tapestry.form;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.tapestry.Tapestry;
+
+/**
+ * A utility class often used with the {@link org.apache.tapestry.form.ListEdit}component. A
+ * ListEditMap is loaded with data objects before the ListEdit renders, and again before the
+ * ListEdit rewinds. This streamlines the synchronization of the form against data on the server. It
+ * is most useful when the set of objects is of a manageable size (say, no more than a few hundred
+ * objects).
+ * <p>
+ * The map stores a list of keys, and relates each key to a value. It also tracks a deleted flag for
+ * each key.
+ * <p>
+ * Usage: <br>
+ * The page or component should implement {@link org.apache.tapestry.event.PageRenderListener}and
+ * implement
+ * {@link org.apache.tapestry.event.PageRenderListener#pageBeginRender(org.apache.tapestry.event.PageEvent)}
+ * to initialize the map.
+ * <p>
+ * The external data (from which keys and values are obtained) is queried, and each key/value pair
+ * is {@link #add(Object, Object) added}to the map, in the order that items should be presented.
+ * <p>
+ * The {@link org.apache.tapestry.form.ListEdit}'s source parameter should be bound to the map's
+ * {@link #getKeys() keys}property. The value parameter should be bound to the map's
+ * {@link #setKey(Object) key}property.
+ * <p>
+ * The {@link org.apache.tapestry.form.ListEdit}'s listener parameter should be bound to a listener
+ * method to synchronize a property of the component from the map.
+ * 
+ * <pre><code>
+ * 
+ *  
+ *   
+ *     public void synchronize({@link org.apache.tapestry.IRequestCycle} cycle)
+ *     {
+ *        ListEditMap map = ...;
+ *        &lt;i&gt;Type&lt;/i&gt; object = (&lt;i&gt;Type&lt;/i&gt;)map.getValue();
+ *    
+ *        if (object == null)
+ *          ...
+ *    
+ *        set&lt;i&gt;Property&lt;/i&gt;(object);
+ *     }
+ *     
+ *   
+ *  
+ * </code></pre>
+ * 
+ * <p>
+ * You may also connect a {@link org.apache.tapestry.form.Checkbox}'s selected parameter to the
+ * map's {@link #isDeleted() deleted}property.
+ * <p>
+ * You may track inclusion in other sets by subclasses and implementing new boolean properties. The
+ * accessor method should be a call to {@link #checkSet(Set)}and the mutator method should be a
+ * call to {@link #updateSet(Set, boolean)}.
+ * 
+ * @author Howard Lewis Ship
+ * @since 3.0
+ */
+
+public class ListEditMap
+{
+    private Map _map = new HashMap();
+
+    private List _keys = new ArrayList();
+
+    private Set _deletedKeys;
+
+    private Object _currentKey;
+
+    /**
+     * Records the key and value into this map. The keys may be obtained, in the order in which they
+     * are added, using {@link #getKeys()}. This also sets the current key (so that you may invoke
+     * {@link #setDeleted(boolean)}, for example).
+     */
+
+    public void add(Object key, Object value)
+    {
+        _currentKey = key;
+
+        _keys.add(_currentKey);
+        _map.put(_currentKey, value);
+    }
+
+    /**
+     * Returns a List of keys, in the order that keys were added to the map (using
+     * {@link #add(Object, Object)}. The caller must not modify the List.
+     */
+
+    public List getKeys()
+    {
+        return _keys;
+    }
+
+    /**
+     * Sets the key for the map. This defines the key used with the other methods:
+     * {@link #getValue()},{@link #isDeleted()},{@link #setDeleted(boolean)}.
+     */
+
+    public void setKey(Object key)
+    {
+        _currentKey = key;
+    }
+
+    /**
+     * Returns the current key within the map.
+     */
+
+    public Object getKey()
+    {
+        return _currentKey;
+    }
+
+    /**
+     * Returns the value for the key (set using {@link #setKey(Object)}). May return null if no
+     * such key has been added (this can occur if a data object is deleted between the time a form
+     * is rendered and the time a form is submitted).
+     */
+
+    public Object getValue()
+    {
+        return _map.get(_currentKey);
+    }
+
+    /**
+     * Returns true if the {@link #setKey(Object) current key}is in the set of deleted keys.
+     */
+
+    public boolean isDeleted()
+    {
+        return checkSet(_deletedKeys);
+    }
+
+    /**
+     * Returns true if the set contains the {@link #getKey() current key}. Returns false is the set
+     * is null, or doesn't contain the current key.
+     */
+
+    protected boolean checkSet(Set set)
+    {
+        if (set == null)
+            return false;
+
+        return set.contains(_currentKey);
+    }
+
+    /**
+     * Adds or removes the {@link #setKey(Object) current key}from the set of deleted keys.
+     */
+
+    public void setDeleted(boolean value)
+    {
+        _deletedKeys = updateSet(_deletedKeys, value);
+    }
+
+    /**
+     * Updates the set, adding or removing the {@link #getKey() current key}from it. Returns the
+     * set passed in. If the value is true and the set is null, an new instance of {@link HashSet}
+     * is created and returned.
+     */
+
+    protected Set updateSet(Set set, boolean value)
+    {
+        if (value)
+        {
+            if (set == null)
+                set = new HashSet();
+
+            set.add(_currentKey);
+        }
+        else
+        {
+            if (set != null)
+                set.remove(_currentKey);
+        }
+
+        return set;
+    }
+
+    /**
+     * Returns the deleted keys in an unspecified order. May return an empty list.
+     */
+
+    public List getDeletedKeys()
+    {
+        return convertSetToList(_deletedKeys);
+    }
+
+    protected List convertSetToList(Set set)
+    {
+        if (Tapestry.isEmpty(set))
+            return Collections.EMPTY_LIST;
+
+        return new ArrayList(set);
+    }
+
+    /**
+     * Returns all the values stored in the map, in the order in which values were added to the map
+     * using {@link #add(Object, Object)}.
+     */
+
+    public List getAllValues()
+    {
+        int count = _keys.size();
+        List result = new ArrayList(count);
+
+        for (int i = 0; i < count; i++)
+        {
+            Object key = _keys.get(i);
+            Object value = _map.get(key);
+
+            result.add(value);
+        }
+
+        return result;
+    }
+
+    /**
+     * Returns all the values stored in the map, excluding those whose id has been marked deleted,
+     * in the order in which values were added to the map using {@link #add(Object, Object)}.
+     */
+
+    public List getValues()
+    {
+        int deletedCount = Tapestry.size(_deletedKeys);
+
+        if (deletedCount == 0)
+            return getAllValues();
+
+        int count = _keys.size();
+
+        List result = new ArrayList(count - deletedCount);
+
+        for (int i = 0; i < count; i++)
+        {
+            Object key = _keys.get(i);
+
+            if (_deletedKeys.contains(key))
+                continue;
+
+            Object value = _map.get(key);
+            result.add(value);
+        }
+
+        return result;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/Option.java b/tapestry-framework/src/org/apache/tapestry/form/Option.java
new file mode 100644
index 0000000..0959e3e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/Option.java
@@ -0,0 +1,97 @@
+//  Copyright 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.tapestry.form;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  A component that renders an HTML &lt;option&gt; form element.
+ *  Such a component must be wrapped (possibly indirectly)
+ *  inside a {@link Select} component.
+ *
+ *  [<a href="../../../../../ComponentReference/Option.html">Component Reference</a>]
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public abstract class Option extends AbstractComponent
+{
+    /**
+     *  Renders the &lt;option&gt; element, or responds when the form containing the element 
+     *  is submitted (by checking {@link Form#isRewinding()}.
+     *
+     *  <p>If the <code>label</code> property is set, it is inserted inside the
+     *  &lt;option&gt; element.
+     *
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        Select select = Select.get(cycle);
+        if (select == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("Option.must-be-contained-by-select"),
+                this,
+                null,
+                null);
+
+        // It isn't enough to know whether the cycle in general is rewinding, need to know
+        // specifically if the form which contains this component is rewinding.
+
+        boolean rewinding = select.isRewinding();
+
+        String value = select.getNextOptionId();
+
+        if (rewinding)
+        {
+            if (!select.isDisabled())
+                getSelectedBinding().setBoolean(select.isSelected(value));
+
+            renderBody(writer, cycle);
+        }
+        else
+        {
+            writer.begin("option");
+
+            writer.attribute("value", value);
+
+            if (getSelectedBinding().getBoolean())
+                writer.attribute("selected", "selected");
+
+            renderInformalParameters(writer, cycle);
+
+            String label = getLabel();
+
+            if (label != null)
+                writer.print(label);
+
+            renderBody(writer, cycle);
+
+            writer.end();
+        }
+
+    }
+
+    public abstract IBinding getSelectedBinding();
+
+    public abstract String getLabel();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/Option.jwc b/tapestry-framework/src/org/apache/tapestry/form/Option.jwc
new file mode 100644
index 0000000..06040e4
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/Option.jwc
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.form.Option" allow-body="yes">
+
+  <description>
+  A single option within a Select.
+  </description>
+  
+  <parameter name="selected" type="java.lang.Boolean" required="yes"/>  
+  <parameter name="label" type="java.lang.String" direction="in"/>
+  
+  <reserved-parameter name="value"/>
+  
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/form/PropertySelection.java b/tapestry-framework/src/org/apache/tapestry/form/PropertySelection.java
new file mode 100644
index 0000000..fb19c52
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/PropertySelection.java
@@ -0,0 +1,248 @@
+//  Copyright 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.tapestry.form;
+
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  A component used to render a drop-down list of options that
+ *  the user may select.
+ * 
+ *  [<a href="../../../../../ComponentReference/PropertySelection.html">Component Reference</a>]
+ *
+ *  <p>Earlier versions of PropertySelection (through release 2.2)
+ *  were more flexible, they included a <b>renderer</b> property
+ *  that controlled how the selection was rendered.  Ultimately,
+ *  this proved of little value and this portion of
+ *  functionality was deprecated in 2.3 and will be removed in 2.3.
+ * 
+ *  <p>Typically, the values available to be selected
+ *  are defined using an {@link org.apache.commons.lang.enum.Enum}.
+ *  A PropertySelection is dependent on
+ *  an {@link IPropertySelectionModel} to provide the list of possible values.
+ *
+ *  <p>Often, this is used to select a particular 
+ *  {@link org.apache.commons.lang.enum.Enum} to assign to a property; the
+ *  {@link EnumPropertySelectionModel} class simplifies this.
+ *
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public abstract class PropertySelection extends AbstractFormComponent
+{
+    /**
+     *  A shared instance of {@link SelectPropertySelectionRenderer}.
+     *
+     * 
+     **/
+
+    public static final IPropertySelectionRenderer DEFAULT_SELECT_RENDERER =
+        new SelectPropertySelectionRenderer();
+
+    /**
+     *  A shared instance of {@link RadioPropertySelectionRenderer}.
+     *
+     * 
+     **/
+
+    public static final IPropertySelectionRenderer DEFAULT_RADIO_RENDERER =
+        new RadioPropertySelectionRenderer();
+
+    /**
+     *  Renders the component, much of which is the responsiblity
+     *  of the {@link IPropertySelectionRenderer renderer}.  The possible options,
+     *  thier labels, and the values to be encoded in the form are provided
+     *  by the {@link IPropertySelectionModel model}.
+     *
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        IForm form = getForm(cycle);
+
+        boolean rewinding = form.isRewinding();
+
+        String name = form.getElementId(this);
+
+        if (rewinding)
+        {
+            // If disabled, ignore anything that comes up from the client.
+
+            if (isDisabled())
+                return;
+
+            String optionValue = cycle.getRequestContext().getParameter(name);
+
+            Object value = (optionValue == null) ? null : getModel().translateValue(optionValue);
+
+            setValue(value);
+
+            return;
+        }
+
+        IPropertySelectionRenderer renderer = getRenderer();
+
+        if (renderer != null)
+        {
+            renderWithRenderer(writer, cycle, renderer);
+            return;
+        }
+
+        writer.begin("select");
+        writer.attribute("name", name);
+
+        if (isDisabled())
+            writer.attribute("disabled", "disabled");
+
+        if (getSubmitOnChange())
+            writer.attribute("onchange", "javascript:this.form.submit();");
+
+        // Apply informal attributes.
+
+        renderInformalParameters(writer, cycle);
+
+        writer.println();
+
+        IPropertySelectionModel model = getModel();
+
+        if (model == null)
+            throw Tapestry.createRequiredParameterException(this, "model");
+
+        int count = model.getOptionCount();
+        boolean foundSelected = false;
+        boolean selected = false;
+        Object value = getValue();
+
+        for (int i = 0; i < count; i++)
+        {
+            Object option = model.getOption(i);
+
+            if (!foundSelected)
+            {
+                selected = isEqual(option, value);
+                if (selected)
+                    foundSelected = true;
+            }
+
+            writer.begin("option");
+            writer.attribute("value", model.getValue(i));
+
+            if (selected)
+                writer.attribute("selected", "selected");
+
+            writer.print(model.getLabel(i));
+
+            writer.end();
+
+            writer.println();
+
+            selected = false;
+        }
+
+        writer.end(); // <select>
+
+    }
+
+    /**
+     *  Renders the property selection using a {@link IPropertySelectionRenderer}.
+     *  Support for this will be removed in 2.3.
+     * 
+     **/
+
+    private void renderWithRenderer(
+        IMarkupWriter writer,
+        IRequestCycle cycle,
+        IPropertySelectionRenderer renderer)
+    {
+        renderer.beginRender(this, writer, cycle);
+
+        IPropertySelectionModel model = getModel();
+
+        int count = model.getOptionCount();
+
+        boolean foundSelected = false;
+        boolean selected = false;
+
+        Object value = getValue();
+
+        for (int i = 0; i < count; i++)
+        {
+            Object option = model.getOption(i);
+
+            if (!foundSelected)
+            {
+                selected = isEqual(option, value);
+                if (selected)
+                    foundSelected = true;
+            }
+
+            renderer.renderOption(this, writer, cycle, model, option, i, selected);
+
+            selected = false;
+        }
+
+        // A PropertySelection doesn't allow a body, so no need to worry about
+        // wrapped components.
+
+        renderer.endRender(this, writer, cycle);
+    }
+
+    private boolean isEqual(Object left, Object right)
+    {
+        // Both null, or same object, then are equal
+
+        if (left == right)
+            return true;
+
+        // If one is null, the other isn't, then not equal.
+
+        if (left == null || right == null)
+            return false;
+
+        // Both non-null; use standard comparison.
+
+        return left.equals(right);
+    }
+
+    public abstract IPropertySelectionModel getModel();
+
+    public abstract IPropertySelectionRenderer getRenderer();
+
+    /** @since 2.2 **/
+
+    public abstract boolean getSubmitOnChange();
+
+    /** @since 2.2 **/
+
+    public abstract Object getValue();
+
+    /** @since 2.2 **/
+
+    public abstract void setValue(Object value);
+
+    /**
+     *  Returns true if this PropertySelection's disabled parameter yields true.
+     *  The corresponding HTML control(s) should be disabled.
+     **/
+
+    public abstract boolean isDisabled();
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/PropertySelection.jwc b/tapestry-framework/src/org/apache/tapestry/form/PropertySelection.jwc
new file mode 100644
index 0000000..4a88edc
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/PropertySelection.jwc
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.form.PropertySelection" 
+	allow-body="no" 
+	allow-informal-parameters="yes">
+  
+  <description>
+  Creates an HTML select to choose a single property from a list of options.
+  </description>
+  
+  <parameter name="value" required="yes" type="java.lang.Object" direction="form"/>
+  
+  <parameter name="model" 
+  	type="org.apache.tapestry.form.IPropertySelectionModel" 
+  	required="yes"
+  	direction="in"/>
+  	
+  <parameter name="disabled"
+  	type="boolean"
+  	direction="in"/>
+  	
+  <parameter name="renderer" 
+  	type="org.apache.tapestry.form.IPropertySelectionRenderer"
+  	direction="in">
+  	<description>
+	An alternate rendered for the property selection.
+  	</description>
+  </parameter>
+  
+  <parameter name="submitOnChange"
+  	type="boolean"
+  	direction="in">
+  	<description>
+  	Enables logic to submit containing form when value changes.
+  	</description>
+  </parameter>
+  	
+  <reserved-parameter name="name"/>
+
+  <property-specification name="name" type="java.lang.String"/>
+  <property-specification name="form" type="org.apache.tapestry.IForm"/>
+    
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/form/Radio.java b/tapestry-framework/src/org/apache/tapestry/form/Radio.java
new file mode 100644
index 0000000..120be4a
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/Radio.java
@@ -0,0 +1,105 @@
+//  Copyright 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.tapestry.form;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Implements a component that manages an HTML &lt;input type=radio&gt; form element.
+ *  Such a component must be wrapped (possibly indirectly)
+ *  inside a {@link RadioGroup} component.
+ *
+ *  [<a href="../../../../../ComponentReference/Radio.html">Component Reference</a>]
+ *
+ * 
+ *  <p>{@link Radio} and {@link RadioGroup} are generally not used (except
+ *  for very special cases).  Instead, a {@link PropertySelection} component is used.
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public abstract class Radio extends AbstractComponent
+{
+    /**
+     *  Renders the form element, or responds when the form containing the element
+     *  is submitted (by checking {@link Form#isRewinding()}.
+     *
+     *
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+
+        RadioGroup group = RadioGroup.get(cycle);
+        if (group == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("Radio.must-be-contained-by-group"),
+                this,
+                null,
+                null);
+
+        // The group determines rewinding from the form.
+
+        boolean rewinding = group.isRewinding();
+
+        int option = group.getNextOptionId();
+
+        if (rewinding)
+        {
+            // If not disabled and this is the selected button within the radio group,
+            // then update set the selection from the group to the value for this
+            // radio button.  This will update the selected parameter of the RadioGroup.
+
+            if (!isDisabled() && !group.isDisabled() && group.isSelected(option))
+                group.updateSelection(getValue());
+            return;
+        }
+
+        writer.beginEmpty("input");
+
+        writer.attribute("type", "radio");
+
+        writer.attribute("name", group.getName());
+
+        // As the group if the value for this Radio matches the selection
+        // for the group as a whole; if so this is the default radio and is checked.
+
+        if (group.isSelection(getValue()))
+            writer.attribute("checked", "checked");
+
+        if (isDisabled() || group.isDisabled())
+            writer.attribute("disabled", "disabled");
+
+        // The value for the Radio matches the option number (provided by the RadioGroup).
+        // When the form is submitted, the RadioGroup will know which option was,
+        // in fact, selected by the user.
+
+        writer.attribute("value", option);
+
+        renderInformalParameters(writer, cycle);
+
+    }
+
+    public abstract boolean isDisabled();
+
+    public abstract Object getValue();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/Radio.jwc b/tapestry-framework/src/org/apache/tapestry/form/Radio.jwc
new file mode 100644
index 0000000..5785794
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/Radio.jwc
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.form.Radio" allow-body="no">
+
+  <description>
+  A single possible selection within a RadioGroup.
+  </description>
+  
+  <parameter name="value" type="java.lang.Object" direction="in"/>
+  
+  <parameter name="disabled" type="boolean" direction="in"/>
+  
+  <reserved-parameter name="checked"/>
+  <reserved-parameter name="type"/>
+  <reserved-parameter name="name"/>
+  
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/form/RadioGroup.java b/tapestry-framework/src/org/apache/tapestry/form/RadioGroup.java
new file mode 100644
index 0000000..ca441ac
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/RadioGroup.java
@@ -0,0 +1,196 @@
+//  Copyright 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.tapestry.form;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  A special type of form component that is used to contain {@link Radio}
+ *  components.  The Radio and {@link Radio} group components work together to
+ *  update a property of some other object, much like a more flexible
+ *  version of a {@link PropertySelection}.
+ *
+ *  [<a href="../../../../../ComponentReference/RadioGroup.html">Component Reference</a>]
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public abstract class RadioGroup extends AbstractFormComponent
+{
+    // Cached copy of the value from the selectedBinding
+    private Object _selection;
+
+    // The value from the HTTP request indicating which
+    // Radio was selected by the user.
+    private int _selectedOption;
+
+    private boolean _rewinding;
+    private boolean _rendering;
+    private int _nextOptionId;
+
+    /**
+     *  A <code>RadioGroup</code> places itself into the {@link IRequestCycle} as
+     *  an attribute, so that its wrapped {@link Radio} components can identify thier
+     *  state.
+     *
+     **/
+
+    private static final String ATTRIBUTE_NAME = "org.apache.tapestry.active.RadioGroup";
+
+    public static RadioGroup get(IRequestCycle cycle)
+    {
+        return (RadioGroup) cycle.getAttribute(ATTRIBUTE_NAME);
+    }
+
+    public abstract IBinding getSelectedBinding();
+
+    public int getNextOptionId()
+    {
+        if (!_rendering)
+            throw Tapestry.createRenderOnlyPropertyException(this, "nextOptionId");
+
+        return _nextOptionId++;
+    }
+
+    /**
+     *  Used by {@link Radio} components wrapped by this <code>RadioGroup</code> to see
+     *  if the group as a whole is disabled.
+     *
+     **/
+
+    public abstract boolean isDisabled();
+
+    public boolean isRewinding()
+    {
+        if (!_rendering)
+            throw Tapestry.createRenderOnlyPropertyException(this, "rewinding");
+
+        return _rewinding;
+    }
+
+    /**
+     *  Returns true if the value is equal to the current selection for the
+     *  group.  This is invoked by a {@link Radio} during rendering
+     *  to determine if it should be marked 'checked'.
+     *
+     **/
+
+    public boolean isSelection(Object value)
+    {
+        if (!_rendering)
+            throw Tapestry.createRenderOnlyPropertyException(this, "selection");
+
+        if (_selection == value)
+            return true;
+
+        if (_selection == null || value == null)
+            return false;
+
+        return _selection.equals(value);
+    }
+
+    /**
+    *  Invoked by the {@link Radio} which is selected to update the 
+    *  property bound to the selected parameter.
+    *
+    **/
+
+    public void updateSelection(Object value)
+    {
+        getSelectedBinding().setObject(value);
+    }
+
+    /**
+    *  Used by {@link Radio} components when rewinding to see if their value was submitted.
+    *
+    **/
+
+    public boolean isSelected(int option)
+    {
+        return _selectedOption == option;
+    }
+
+    /**
+     * Doesn't actual render an HTML element as there is no direct equivalent for
+     * an HTML element.  A <code>RadioGroup</code> component exists to organize the
+     * {@link Radio} components it wraps (directly or indirectly).
+     *
+     * A {@link Radio} can finds its {@link RadioGroup} as a {@link IRequestCycle} attribute.
+     *
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        IForm form = getForm(cycle);
+
+        if (cycle.getAttribute(ATTRIBUTE_NAME) != null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("RadioGroup.may-not-nest"),
+                this,
+                null,
+                null);
+
+        // It isn't enough to know whether the cycle in general is rewinding, need to know
+        // specifically if the form which contains this component is rewinding.
+
+        _rewinding = form.isRewinding();
+
+        // Used whether rewinding or not.
+
+        String name = form.getElementId(this);
+
+        cycle.setAttribute(ATTRIBUTE_NAME, this);
+
+        // When rewinding, find out which (if any) radio was selected by
+        // the user.
+
+        if (_rewinding)
+        {
+            String value = cycle.getRequestContext().getParameter(name);
+            if (value == null)
+                _selectedOption = -1;
+            else
+                _selectedOption = Integer.parseInt(value);
+        }
+
+        try
+        {
+            _rendering = true;
+            _nextOptionId = 0;
+
+            // For rendering, the Radio components need to know what the current
+            // selection is, so that the correct one can mark itself 'checked'.
+
+            if (!_rewinding)
+                _selection = getSelectedBinding().getObject();
+
+            renderBody(writer, cycle);
+        }
+        finally
+        {
+            _rendering = false;
+            _selection = null;
+        }
+
+        cycle.removeAttribute(ATTRIBUTE_NAME);
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/RadioGroup.jwc b/tapestry-framework/src/org/apache/tapestry/form/RadioGroup.jwc
new file mode 100644
index 0000000..2f3f6dd
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/RadioGroup.jwc
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.form.RadioGroup" allow-informal-parameters="no">
+
+  <description>
+  Groups together a number of Radio components.
+  </description>
+  
+  <parameter name="selected" required="yes"/>
+  
+  <parameter name="disabled" type="boolean" direction="in"/>
+  
+  <property-specification name="name" type="java.lang.String"/>
+  <property-specification name="form" type="org.apache.tapestry.IForm"/>
+  
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/form/RadioPropertySelectionRenderer.java b/tapestry-framework/src/org/apache/tapestry/form/RadioPropertySelectionRenderer.java
new file mode 100644
index 0000000..8eb27a2
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/RadioPropertySelectionRenderer.java
@@ -0,0 +1,95 @@
+//  Copyright 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.tapestry.form;
+
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  Implementation of {@link IPropertySelectionRenderer} that
+ *  produces a table of radio (&lt;input type=radio&gt;) elements.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public class RadioPropertySelectionRenderer implements IPropertySelectionRenderer
+{
+
+    /**
+     *  Writes the &lt;table&gt; element.
+     *
+     **/
+
+    public void beginRender(PropertySelection component, IMarkupWriter writer, IRequestCycle cycle)
+    {
+        writer.begin("table");
+        writer.attribute("border", 0);
+        writer.attribute("cellpadding", 0);
+        writer.attribute("cellspacing", 2);
+    }
+
+    /**
+     *  Closes the &lt;table&gt; element.
+     *
+     **/
+
+    public void endRender(PropertySelection component, IMarkupWriter writer, IRequestCycle cycle)
+    {
+        writer.end(); // <table>
+    }
+
+    /**
+     *  Writes a row of the table.  The table contains two cells; the first is the radio
+     *  button, the second is the label for the radio button.
+     *
+     **/
+
+    public void renderOption(
+        PropertySelection component,
+        IMarkupWriter writer,
+        IRequestCycle cycle,
+        IPropertySelectionModel model,
+        Object option,
+        int index,
+        boolean selected)
+    {
+        writer.begin("tr");
+        writer.begin("td");
+
+        writer.beginEmpty("input");
+        writer.attribute("type", "radio");
+        writer.attribute("name", component.getName());
+        writer.attribute("value", model.getValue(index));
+
+        if (component.isDisabled())
+            writer.attribute("disabled", "disabled");
+
+        if (selected)
+            writer.attribute("checked", "checked");
+
+        writer.end(); // <td>
+
+        writer.println();
+
+        writer.begin("td");
+        writer.print(model.getLabel(index));
+        writer.end(); // <td>
+        writer.end(); // <tr>	
+
+        writer.println();
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/Select.java b/tapestry-framework/src/org/apache/tapestry/form/Select.java
new file mode 100644
index 0000000..fe2bedf
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/Select.java
@@ -0,0 +1,187 @@
+//  Copyright 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.tapestry.form;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.request.RequestContext;
+
+/**
+ *  Implements a component that manages an HTML &lt;select&gt; form element.
+ *  The most common situation, using a &lt;select&gt; to set a specific
+ *  property of some object, is best handled using a {@link PropertySelection} component.
+ *
+ *  [<a href="../../../../../ComponentReference/Select.html">Component Reference</a>]
+ * 
+ *  <p>Otherwise, this component is very similar to {@link RadioGroup}.
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public abstract class Select extends AbstractFormComponent
+{
+    private boolean _rewinding;
+    private boolean _rendering;
+
+    private Set _selections;
+    private int _nextOptionId;
+
+    /**
+     *  Used by the <code>Select</code> to record itself as a
+     *  {@link IRequestCycle} attribute, so that the
+     *  {@link Option} components it wraps can have access to it.
+     *
+     **/
+
+    private final static String ATTRIBUTE_NAME = "org.apache.tapestry.active.Select";
+
+    public static Select get(IRequestCycle cycle)
+    {
+        return (Select) cycle.getAttribute(ATTRIBUTE_NAME);
+    }
+
+    public abstract boolean isDisabled();
+
+    public abstract boolean isMultiple();
+
+    public boolean isRewinding()
+    {
+        if (!_rendering)
+            throw Tapestry.createRenderOnlyPropertyException(this, "rewinding");
+
+        return _rewinding;
+    }
+
+    public String getNextOptionId()
+    {
+        if (!_rendering)
+            throw Tapestry.createRenderOnlyPropertyException(this, "nextOptionId");
+
+        // Return it as a hex value.
+
+        return Integer.toString(_nextOptionId++);
+    }
+
+    public boolean isSelected(String value)
+    {
+        if (_selections == null)
+            return false;
+
+        return _selections.contains(value);
+    }
+
+    /**
+     *  Renders the &lt;option&gt; element, or responds when the form containing the element
+     *  is submitted (by checking {@link IForm#isRewinding()}.
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        IForm form = getForm(cycle);
+
+        if (cycle.getAttribute(ATTRIBUTE_NAME) != null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("Select.may-not-nest"),
+                this,
+                null,
+                null);
+
+        // It isn't enough to know whether the cycle in general is rewinding, need to know
+        // specifically if the form which contains this component is rewinding.
+
+        _rewinding = form.isRewinding();
+
+        // Used whether rewinding or not.
+
+        String name = form.getElementId(this);
+
+        cycle.setAttribute(ATTRIBUTE_NAME, this);
+
+        if (_rewinding)
+        {
+            _selections = buildSelections(cycle, name);
+        }
+        else
+        {
+            writer.begin("select");
+
+            writer.attribute("name", name);
+
+            if (isMultiple())
+                writer.attribute("multiple", "multiple");
+
+            if (isDisabled())
+                writer.attribute("disabled", "disabled");
+
+            renderInformalParameters(writer, cycle);
+        }
+
+        _rendering = true;
+        _nextOptionId = 0;
+
+        renderBody(writer, cycle);
+
+        if (!_rewinding)
+        {
+            writer.end();
+        }
+
+        cycle.removeAttribute(ATTRIBUTE_NAME);
+
+    }
+
+    protected void cleanupAfterRender(IRequestCycle cycle)
+    {
+        _rendering = false;
+        _selections = null;
+
+        super.cleanupAfterRender(cycle);
+    }
+
+    /**
+     *  Cut-and-paste with {@link RadioGroup}!
+     *
+     **/
+
+    private Set buildSelections(IRequestCycle cycle, String parameterName)
+    {
+        RequestContext context = cycle.getRequestContext();
+
+        String[] parameters = context.getParameters(parameterName);
+
+        if (parameters == null)
+            return null;
+
+        int length = parameters.length;
+
+        int size = (parameters.length > 30) ? 101 : 7;
+
+        Set result = new HashSet(size);
+
+        for (int i = 0; i < length; i++)
+            result.add(parameters[i]);
+
+        return result;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/Select.jwc b/tapestry-framework/src/org/apache/tapestry/form/Select.jwc
new file mode 100644
index 0000000..f8507ed
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/Select.jwc
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN"
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.form.Select">
+
+  <description>
+  Creates an HTML select populated with a number of options.
+  </description>
+
+  <parameter name="multiple" type="boolean" direction="in"/>
+  <parameter name="disabled" type="boolean" direction="in"/>
+
+  <reserved-parameter name="name"/>
+
+  <property-specification name="name" type="java.lang.String"/>
+  <property-specification name="form" type="org.apache.tapestry.IForm"/>
+
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/form/SelectPropertySelectionRenderer.java b/tapestry-framework/src/org/apache/tapestry/form/SelectPropertySelectionRenderer.java
new file mode 100644
index 0000000..0a46ad6
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/SelectPropertySelectionRenderer.java
@@ -0,0 +1,93 @@
+//  Copyright 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.tapestry.form;
+
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  Implementation of {@link IPropertySelectionRenderer} that
+ *  produces a &lt;select&gt; element (containing &lt;option&gt; elements).
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public class SelectPropertySelectionRenderer
+    implements IPropertySelectionRenderer
+{
+    /**
+     *  Writes the &lt;select&gt; element.  If the
+     *  {@link PropertySelection} is {@link PropertySelection#isDisabled() disabled}
+     *  then a <code>disabled</code> attribute is written into the tag
+     *  (though Navigator 4 will ignore this).
+     *
+     **/
+
+    public void beginRender(
+        PropertySelection component,
+        IMarkupWriter writer,
+        IRequestCycle cycle)
+    {
+        writer.begin("select");
+        writer.attribute("name", component.getName());
+
+        if (component.isDisabled())
+            writer.attribute("disabled", "disabled");
+
+        writer.println();
+    }
+
+    /**
+     *  Closes the &lt;select&gt; element.
+     *
+     **/
+
+    public void endRender(
+        PropertySelection component,
+        IMarkupWriter writer,
+        IRequestCycle cycle)
+    {
+        writer.end(); // <select>
+    }
+
+    /**
+     *  Writes an &lt;option&gt; element.
+     *
+     **/
+
+    public void renderOption(
+        PropertySelection component,
+        IMarkupWriter writer,
+        IRequestCycle cycle,
+        IPropertySelectionModel model,
+        Object option,
+        int index,
+        boolean selected)
+    {
+        writer.beginEmpty("option");
+        writer.attribute("value", model.getValue(index));
+
+        if (selected)
+            writer.attribute("selected", "selected");
+
+        writer.print(model.getLabel(index));
+
+        writer.end();
+        
+        writer.println();
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/StringPropertySelectionModel.java b/tapestry-framework/src/org/apache/tapestry/form/StringPropertySelectionModel.java
new file mode 100644
index 0000000..f9a591f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/StringPropertySelectionModel.java
@@ -0,0 +1,84 @@
+//  Copyright 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.tapestry.form;
+
+/**
+ *  Implementation of {@link IPropertySelectionModel} that allows one String from
+ *  an array of Strings to be selected as the property.
+ *
+ *  <p>Uses a simple index number as the value (used to represent the selected String).
+ *  This assumes that the possible values for the Strings will remain constant between
+ *  request cycles.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ * 
+ **/
+
+public class StringPropertySelectionModel implements IPropertySelectionModel
+{
+    private String[] options;
+
+    /**
+     * Standard constructor.
+     *
+     * The options are retained (not copied).
+     **/
+
+    public StringPropertySelectionModel(String[] options)
+    {
+        this.options = options;
+    }
+
+    public int getOptionCount()
+    {
+        return options.length;
+    }
+
+    public Object getOption(int index)
+    {
+        return options[index];
+    }
+
+    /**
+     *  Labels match options.
+     *
+     **/
+
+    public String getLabel(int index)
+    {
+        return options[index];
+    }
+
+    /**
+     *  Values are indexes into the array of options.
+     *
+     **/
+
+    public String getValue(int index)
+    {
+        return Integer.toString(index);
+    }
+
+    public Object translateValue(String value)
+    {
+        int index;
+
+        index = Integer.parseInt(value);
+
+        return options[index];
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/Submit.java b/tapestry-framework/src/org/apache/tapestry/form/Submit.java
new file mode 100644
index 0000000..9de335c
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/Submit.java
@@ -0,0 +1,112 @@
+//  Copyright 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.tapestry.form;
+
+import org.apache.tapestry.IActionListener;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+
+
+/**
+ *  Implements a component that manages an HTML &lt;input type=submit&gt; form element.
+ * 
+ *  [<a href="../../../../../ComponentReference/Submit.html">Component Reference</a>]
+ *
+ *  <p>This component is generally only used when the form has multiple
+ *  submit buttons, and it is important for the application to know
+ *  which one was pressed.  You may also want to use
+ *  {@link ImageSubmit} which accomplishes much the same thing, but uses
+ *  a graphic image instead.
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public abstract class Submit extends AbstractFormComponent{
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+
+        IForm form = getForm(cycle);
+		
+        boolean rewinding = form.isRewinding();
+
+        String name = form.getElementId(this);
+
+        if (rewinding)
+        {
+            // Don't bother doing anything if disabled.
+
+            if (isDisabled())
+                return;
+
+            // How to know which Submit button was actually
+            // clicked?  When submitted, it produces a request parameter
+            // with its name and value (the value serves double duty as both
+            // the label on the button, and the parameter value).
+
+            String value = cycle.getRequestContext().getParameter(name);
+
+            // If the value isn't there, then this button wasn't
+            // selected.
+
+            if (value == null)
+                return;
+
+            IBinding selectedBinding = getSelectedBinding();
+
+            if (selectedBinding != null)
+                selectedBinding.setObject(getTag());
+
+            IActionListener listener = getListener();
+
+            if (listener != null)
+                listener.actionTriggered(this, cycle);
+
+            return;
+        }
+
+        writer.beginEmpty("input");
+        writer.attribute("type", "submit");
+        writer.attribute("name", name);
+
+        if (isDisabled())
+            writer.attribute("disabled", "disabled");
+
+        String label = getLabel();
+
+        if (label != null)
+            writer.attribute("value", label);
+
+        renderInformalParameters(writer, cycle);
+
+        writer.closeTag();
+    }
+
+    public abstract String getLabel();
+
+    public abstract IBinding getSelectedBinding();
+
+    public abstract boolean isDisabled();
+
+    public abstract IActionListener getListener();
+
+    public abstract Object getTag();
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/Submit.jwc b/tapestry-framework/src/org/apache/tapestry/form/Submit.jwc
new file mode 100644
index 0000000..b3a8d3f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/Submit.jwc
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.form.Submit" allow-body="no">
+
+  <description>
+  Creates a labeled submit button within a form.
+  </description>
+
+  <parameter name="label" type="java.lang.String" direction="in"/>
+  <parameter name="disabled" type="boolean" direction="in"/>
+  <parameter name="selected"/>
+  <parameter name="tag" type="java.lang.Object" direction="in"/>
+  <parameter name="listener" 
+  	type="org.apache.tapestry.IActionListener"
+  	direction="in"/>
+  
+  <reserved-parameter name="name"/>
+  <reserved-parameter name="type"/>
+
+  <property-specification name="name" type="java.lang.String"/>
+  <property-specification name="form" type="org.apache.tapestry.IForm"/>  
+  
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/form/TextArea.java b/tapestry-framework/src/org/apache/tapestry/form/TextArea.java
new file mode 100644
index 0000000..b6c3516
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/TextArea.java
@@ -0,0 +1,88 @@
+//  Copyright 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.tapestry.form;
+
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  Implements a component that manages an HTML &lt;textarea&gt; form element.
+ *
+ *  [<a href="../../../../../ComponentReference/TextArea.html">Component Reference</a>]
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public abstract class TextArea extends AbstractFormComponent
+{
+
+    /**
+     *  Renders the form element, or responds when the form containing the element
+     *  is submitted (by checking {@link Form#isRewinding()}.
+     *
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        IForm form = getForm(cycle);
+		
+        // It isn't enough to know whether the cycle in general is rewinding, need to know
+        // specifically if the form which contains this component is rewinding.
+
+        boolean rewinding = form.isRewinding();
+
+        // Used whether rewinding or not.
+
+        String name = form.getElementId(this);
+
+        if (rewinding)
+        {
+        	if (!isDisabled())
+	            setValue(cycle.getRequestContext().getParameter(name));
+
+            return;
+        }
+        
+        if (cycle.isRewinding())
+        	return;
+
+        writer.begin("textarea");
+
+        writer.attribute("name", name);
+
+        if (isDisabled())
+            writer.attribute("disabled", "disabled");
+
+        renderInformalParameters(writer, cycle);
+
+        String value = getValue();
+
+        if (value != null)
+            writer.print(value);
+
+        writer.end();
+
+    }
+
+    public abstract boolean isDisabled();
+
+    public abstract String getValue();
+
+    public abstract void setValue(String value);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/TextArea.jwc b/tapestry-framework/src/org/apache/tapestry/form/TextArea.jwc
new file mode 100644
index 0000000..416e584
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/TextArea.jwc
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.form.TextArea" allow-body="no">
+
+  <description>
+  A multi-line text area.
+  </description>
+  
+  <parameter name="value" type="java.lang.String" required="yes" direction="form"/>
+  
+  <parameter name="disabled" type="boolean" direction="in"/>
+  
+  <reserved-parameter name="name"/>
+  
+  <property-specification name="name" type="java.lang.String"/>
+  <property-specification name="form" type="org.apache.tapestry.IForm"/>
+  
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/form/TextField.java b/tapestry-framework/src/org/apache/tapestry/form/TextField.java
new file mode 100644
index 0000000..f632e56
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/TextField.java
@@ -0,0 +1,43 @@
+//  Copyright 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.tapestry.form;
+
+import org.apache.tapestry.IBinding;
+
+/**
+ *  Implements a component that manages an HTML &lt;input type=text&gt; or
+ *  &lt;input type=password&gt; form element.
+ *
+ *  [<a href="../../../../../ComponentReference/TextField.html">Component Reference</a>]
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public abstract class TextField extends AbstractTextField
+{
+    public abstract IBinding getValueBinding();
+
+    public String readValue()
+    {
+        return (String) getValueBinding().getObject("value", String.class);
+    }
+
+    public void updateValue(String value)
+    {
+        getValueBinding().setString(value);
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/TextField.jwc b/tapestry-framework/src/org/apache/tapestry/form/TextField.jwc
new file mode 100644
index 0000000..d00208d
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/TextField.jwc
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.form.TextField" allow-body="no">
+
+  <description>
+  A text input field.
+  </description>
+  
+  <parameter name="value" type="java.lang.String" required="yes"/>
+  
+  <parameter name="disabled" type="boolean" direction="in"/>
+  
+  <parameter name="hidden" type="boolean" direction="in"/>
+  
+  <reserved-parameter name="name"/>
+  <reserved-parameter name="type"/>
+  <reserved-parameter name="value"/>
+  
+  <property-specification name="name" type="java.lang.String"/>
+  <property-specification name="form" type="org.apache.tapestry.IForm"/>
+  
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/form/Upload.java b/tapestry-framework/src/org/apache/tapestry/form/Upload.java
new file mode 100644
index 0000000..e71dbc1
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/Upload.java
@@ -0,0 +1,72 @@
+//  Copyright 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.tapestry.form;
+
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.request.IUploadFile;
+
+/**
+ *  Form element used to upload files.  For the momement, it is necessary to
+ *  explicitly set the form's enctype to "multipart/form-data".
+ * 
+ *  [<a href="../../../../../ComponentReference/Upload.html">Component Reference</a>]
+ * 
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.8
+ * 
+ **/
+
+public abstract class Upload extends AbstractFormComponent
+{
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        IForm form = getForm(cycle);
+		
+        String name = form.getElementId(this);
+
+        if (form.isRewinding())
+        {
+        	if (!isDisabled())
+	            setFile(cycle.getRequestContext().getUploadFile(name));
+
+            return;
+        }
+
+		// Force the form to use the correct encoding type for
+		// file uploads.
+		
+		form.setEncodingType("multipart/form-data");
+
+        writer.beginEmpty("input");
+        writer.attribute("type", "file");
+        writer.attribute("name", name);
+
+        if (isDisabled())
+            writer.attribute("disabled", "disabled");
+
+        // Size, width, etc. can be specified as informal parameters
+        // (Not making the same mistake here that was made with TextField
+        // and friends).
+
+        renderInformalParameters(writer, cycle);
+    }
+
+    public abstract boolean isDisabled();
+
+    public abstract void setFile(IUploadFile file);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/form/Upload.jwc b/tapestry-framework/src/org/apache/tapestry/form/Upload.jwc
new file mode 100644
index 0000000..d082cbe
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/Upload.jwc
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+
+<!-- $Id$ -->
+
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+	
+<component-specification class="org.apache.tapestry.form.Upload" allow-body="no">
+
+  <description>
+  Allows a file to be uploaded as part of a form.
+  </description>
+
+  <parameter name="file" type="org.apache.tapestry.request.IUploadFile" required="yes" direction="form">
+  	<description>
+  	Parameter updated with the information (filename and content) of the file
+  	when the form is submitted.
+  	</description>
+  </parameter>
+
+  <parameter name="disabled" type="boolean" direction="in"/>
+ 
+  <reserved-parameter name="type"/>
+  <reserved-parameter name="name"/>
+  
+  <property-specification name="name" type="java.lang.String"/>
+  <property-specification name="form" type="org.apache.tapestry.IForm"/>
+  
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/form/package.html b/tapestry-framework/src/org/apache/tapestry/form/package.html
new file mode 100644
index 0000000..e6b9701
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/form/package.html
@@ -0,0 +1,20 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+<p>Components for implementing basic HTML Forms.  Most
+components are straight forward 1:1 mappings between Tapestry components and a
+corresponding HTML element. {@link org.apache.tapestry.form.PropertySelection} is more complicated,
+as it manages way more of the process of implementing a &lt;select&gt; and its &lt;option&gt;s.
+
+<p>Package {@link org.apache.tapestry.valid} contains more complex components that not only collect
+input, but validate it as well.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/html/BasePage.java b/tapestry-framework/src/org/apache/tapestry/html/BasePage.java
new file mode 100644
index 0000000..452b422
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/BasePage.java
@@ -0,0 +1,43 @@
+//  Copyright 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.tapestry.html;
+
+import java.io.OutputStream;
+
+import org.apache.tapestry.AbstractPage;
+import org.apache.tapestry.IMarkupWriter;
+
+/**
+ *  Concrete class for HTML pages. Most pages
+ *  should be able to simply subclass this, adding new properties and
+ *  methods.  An unlikely exception would be a page that was not based
+ *  on a template.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ **/
+
+public class BasePage extends AbstractPage
+{
+    /**
+     *  Returns a new {@link HTMLWriter}.
+     *
+     **/
+
+    public IMarkupWriter getResponseWriter(OutputStream out)
+    {
+        return new HTMLWriter(out, getOutputEncoding());
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/html/Body.java b/tapestry-framework/src/org/apache/tapestry/html/Body.java
new file mode 100644
index 0000000..6dcca9c
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/Body.java
@@ -0,0 +1,406 @@
+//  Copyright 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.tapestry.html;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.IScriptProcessor;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.asset.PrivateAsset;
+import org.apache.tapestry.resource.ClasspathResourceLocation;
+import org.apache.tapestry.util.IdAllocator;
+
+/**
+ *  The body of a Tapestry page.  This is used since it allows components on the
+ *  page access to an initialization script (that is written the start, just inside
+ *  the &lt;body&gt; tag).  This is currently used by {@link Rollover} and {@link Script}
+ *  components.
+ * 
+ *  [<a href="../../../../../ComponentReference/Body.html">Component Reference</a>]
+ * 
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public abstract class Body extends AbstractComponent implements IScriptProcessor
+{
+    // Lines that belong inside the onLoad event handler for the <body> tag.
+    private StringBuffer _initializationScript;
+
+    // The writer initially passed to render() ... wrapped elements render
+    // into a nested response writer.
+
+    private IMarkupWriter _outerWriter;
+
+    // Any other scripting desired
+
+    private StringBuffer _bodyScript;
+
+    // Contains text lines related to image initializations
+
+    private StringBuffer _imageInitializations;
+
+    /**
+     *  Map of URLs to Strings (preloaded image references).
+     *
+     **/
+
+    private Map _imageMap;
+
+    /**
+     *  List of included scripts.  Values are Strings.
+     *
+     *  @since 1.0.5
+     *
+     **/
+
+    private List _externalScripts;
+
+    private IdAllocator _idAllocator;
+
+    private static final String ATTRIBUTE_NAME = "org.apache.tapestry.active.Body";
+
+    /**
+     *  Tracks a particular preloaded image.
+     *
+     **/
+
+    /**
+     *  Adds to the script an initialization for the named variable as
+     *  an Image(), to the given URL.
+     *
+     *  <p>Returns a reference, a string that can be used to represent
+     *  the preloaded image in a JavaScript function.
+     *
+     *  @since 1.0.2
+     **/
+
+    public String getPreloadedImageReference(String URL)
+    {
+        if (_imageMap == null)
+            _imageMap = new HashMap();
+
+        String reference = (String) _imageMap.get(URL);
+
+        if (reference == null)
+        {
+            int count = _imageMap.size();
+            String varName = "tapestry_preload[" + count + "]";
+            reference = varName + ".src";
+
+            if (_imageInitializations == null)
+                _imageInitializations = new StringBuffer();
+
+            _imageInitializations.append("  ");
+            _imageInitializations.append(varName);
+            _imageInitializations.append(" = new Image();\n");
+            _imageInitializations.append("  ");
+            _imageInitializations.append(reference);
+            _imageInitializations.append(" = \"");
+            _imageInitializations.append(URL);
+            _imageInitializations.append("\";\n");
+
+            _imageMap.put(URL, reference);
+        }
+
+        return reference;
+    }
+
+    /**
+     *  Adds other initialization, in the form of additional JavaScript
+     *  code to execute from the &lt;body&gt;'s <code>onLoad</code> event
+     *  handler.  The caller is responsible for adding a semicolon (statement
+     *  terminator).  This method will add a newline after the script.
+     *
+     **/
+
+    public void addInitializationScript(String script)
+    {
+        if (_initializationScript == null)
+            _initializationScript = new StringBuffer(script.length() + 1);
+
+        _initializationScript.append(script);
+        _initializationScript.append('\n');
+
+    }
+
+    /**
+     *  Adds additional scripting code to the page.  This code
+     *  will be added to a large block of scripting code at the
+     *  top of the page (i.e., the before the &lt;body&gt; tag).
+     *
+     *  <p>This is typically used to add some form of JavaScript
+     *  event handler to a page.  For example, the
+     *  {@link Rollover} component makes use of this.
+     *
+     *  <p>Another way this is invoked is by using the
+     *  {@link Script} component.
+     *
+     *  <p>The string will be added, as-is, within
+     *  the &lt;script&gt; block generated by this <code>Body</code> component.
+     *  The script should <em>not</em> contain HTML comments, those will
+     *  be supplied by this Body component.
+     *
+     *  <p>A frequent use is to add an initialization function using
+     *  this method, then cause it to be executed using
+     *  {@link #addInitializationScript(String)}.
+     *
+     **/
+
+    public void addBodyScript(String script)
+    {
+        if (_bodyScript == null)
+            _bodyScript = new StringBuffer(script.length());
+
+        _bodyScript.append(script);
+    }
+
+    /**
+     *  Used to include a script from an outside URL (the scriptLocation
+     *  is a URL, probably obtained from an asset.  This adds
+     *  an &lt;script src="..."&gt; tag before the main
+     *  &lt;script&gt; tag.  The Body component ensures
+     *  that each URL is included only once.
+     *
+     *  @since 1.0.5
+     *
+     **/
+
+    public void addExternalScript(IResourceLocation scriptLocation)
+    {
+        if (_externalScripts == null)
+            _externalScripts = new ArrayList();
+
+        if (_externalScripts.contains(scriptLocation))
+            return;
+
+        // Alas, this won't give a good ILocation for the actual problem.
+
+        if (!(scriptLocation instanceof ClasspathResourceLocation))
+            throw new ApplicationRuntimeException(
+                Tapestry.format("Body.include-classpath-script-only", scriptLocation),
+                this,
+                null,
+                null);
+
+        // Record the URL so we don't include it twice.
+
+        _externalScripts.add(scriptLocation);
+    }
+
+    /**
+     * Writes &lt;script&gt; elements for all the external scripts.
+     */
+    private void writeExternalScripts(IMarkupWriter writer)
+    {
+        int count = Tapestry.size(_externalScripts);
+        for (int i = 0; i < count; i++)
+        {
+            ClasspathResourceLocation scriptLocation =
+                (ClasspathResourceLocation) _externalScripts.get(i);
+
+            // This is still very awkward!  Should move the code inside PrivateAsset somewhere
+            // else, so that an asset does not have to be created to to build the URL.
+            PrivateAsset asset = new PrivateAsset(scriptLocation, null);
+            String url = asset.buildURL(getPage().getRequestCycle());
+
+            // Note: important to use begin(), not beginEmpty(), because browser don't
+            // interpret <script .../> properly.
+
+            writer.begin("script");
+            writer.attribute("language", "JavaScript");
+            writer.attribute("type", "text/javascript");
+            writer.attribute("src", url);
+            writer.end();
+            writer.println();
+        }
+
+    }
+
+    /**
+     *  Retrieves the <code>Body</code> that was stored into the
+     *  request cycle.  This allows components wrapped by the
+     *  <code>Body</code> to locate it and access the services it
+     *  provides.
+     *
+     **/
+
+    public static Body get(IRequestCycle cycle)
+    {
+        return (Body) cycle.getAttribute(ATTRIBUTE_NAME);
+    }
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        if (cycle.getAttribute(ATTRIBUTE_NAME) != null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("Body.may-not-nest"),
+                this,
+                null,
+                null);
+
+        cycle.setAttribute(ATTRIBUTE_NAME, this);
+
+        _outerWriter = writer;
+
+        IMarkupWriter nested = writer.getNestedWriter();
+
+        renderBody(nested, cycle);
+
+        // Start the body tag.
+        writer.println();
+        writer.begin(getElement());
+        renderInformalParameters(writer, cycle);
+
+        writer.println();
+
+        // Write the page's scripting.  This is included scripts
+        // and dynamic JavaScript, including initialization.
+
+        writeScript(_outerWriter);
+
+        // Close the nested writer, which dumps its buffered content
+        // into its parent.
+
+        nested.close();
+
+        writer.end(); // <body>
+
+    }
+
+    protected void cleanupAfterRender(IRequestCycle cycle)
+    {
+        super.cleanupAfterRender(cycle);
+
+        if (_idAllocator != null)
+            _idAllocator.clear();
+
+        if (_imageMap != null)
+            _imageMap.clear();
+
+        if (_externalScripts != null)
+            _externalScripts.clear();
+
+        if (_initializationScript != null)
+            _initializationScript.setLength(0);
+
+        if (_imageInitializations != null)
+            _imageInitializations.setLength(0);
+
+        if (_bodyScript != null)
+            _bodyScript.setLength(0);
+
+        _outerWriter = null;
+    }
+
+    /**
+     *  Writes a single large JavaScript block containing:
+     *  <ul>
+     *  <li>Any image initializations
+     *  <li>Any scripting
+     *  <li>Any initializations
+     *  </ul>
+     *
+     *  <p>The script is written into a nested markup writer.
+     *
+     *  <p>If there are any other initializations 
+     *  (see {@link #addInitializationScript(String)}),
+     *  then a function to execute them is created.
+     **/
+
+    protected void writeScript(IMarkupWriter writer)
+    {
+        if (!Tapestry.isEmpty(_externalScripts))
+            writeExternalScripts(writer);
+
+        if (!(any(_initializationScript) || any(_bodyScript) || any(_imageInitializations)))
+            return;
+
+        writer.begin("script");
+        writer.attribute("language", "JavaScript");
+        writer.attribute("type", "text/javascript");
+        writer.printRaw("<!--");
+
+        if (any(_imageInitializations))
+        {
+            writer.printRaw("\n\nvar tapestry_preload = new Array();\n");
+            writer.printRaw("if (document.images)\n");
+            writer.printRaw("{\n");
+            writer.printRaw(_imageInitializations.toString());
+            writer.printRaw("}\n");
+        }
+
+        if (any(_bodyScript))
+        {
+            writer.printRaw("\n\n");
+            writer.printRaw(_bodyScript.toString());
+        }
+
+        if (any(_initializationScript))
+        {
+
+            writer.printRaw("\n\n" + "window.onload = function ()\n" + "{\n");
+
+            writer.printRaw(_initializationScript.toString());
+
+            writer.printRaw("}");
+        }
+
+        writer.printRaw("\n\n// -->");
+        writer.end();
+    }
+
+    private boolean any(StringBuffer buffer)
+    {
+        if (buffer == null)
+            return false;
+
+        return buffer.length() > 0;
+    }
+
+    public abstract String getElement();
+
+    public abstract void setElement(String element);
+
+    /**
+     * Sets the element parameter property to its default, "body".
+     * 
+     * @since 3.0
+     */
+    protected void finishLoad()
+    {
+        setElement("body");
+    }
+
+    /** @since 3.0 */
+
+    public String getUniqueString(String baseValue)
+    {
+        if (_idAllocator == null)
+            _idAllocator = new IdAllocator();
+
+        return _idAllocator.allocateId(baseValue);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/html/Body.jwc b/tapestry-framework/src/org/apache/tapestry/html/Body.jwc
new file mode 100644
index 0000000..102bc5c
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/Body.jwc
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.html.Body">
+
+  <description>
+  Takes the place of the normal HTML &lt;body&gt; element, providing various
+  forms of support to all components it wraps.
+  </description>
+
+  <parameter
+  	name="element"
+  	type="java.lang.String"
+  	direction="in">
+  	<description>
+  	Name of element to use, defaults to "body".
+  	</description>
+  </parameter>
+ 
+  <reserved-parameter name="onload"/>
+    
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/html/ExceptionDisplay.html b/tapestry-framework/src/org/apache/tapestry/html/ExceptionDisplay.html
new file mode 100644
index 0000000..5a9fa6a
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/ExceptionDisplay.html
@@ -0,0 +1,71 @@
+<!-- $Id$ -->
+<html>
+<link rel="stylesheet" type="text/css" href="../pages/Exception.css">
+
+<body>
+
+<span jwcid="$content$">
+<p>
+
+<table class="exception-display">
+
+<span jwcid="eException">
+	
+		<tr class="exception-name">
+			<td colspan="2"><span jwcid="insertClass">some.exception.Class</span></td>
+		</tr>
+		
+		<tr class="exception-message">
+			<td colspan="2"><span jwcid="insertMessage">A message describing the exception.</span></td>
+		</tr>
+		
+		<tr jwcid="eProperty">
+			<th><span jwcid="insertPropertyName">Property Name</span>:</th>
+			<td><span jwcid="insertPropertyValue">Property Value</span></td>
+		</tr>
+
+		<tr jwcid="$remove$" class="odd">
+			<th>Property Name 2:</th>
+			<td>Property Value 2</td>
+		</tr>
+
+		<tr jwcid="$remove$" class="even">
+			<th>Property Name 3:</th>
+			<td>Property Value 3</td>
+		</tr>
+
+		<tr jwcid="$remove$" class="odd">
+			<th>Property Name 4:</th>
+			<td>Property Value 4</td>
+		</tr>		
+		
+<span jwcid="ifNotLast">
+		<tr> <td colspan=2> &nbsp; </td> </tr>
+</span>
+	
+<span jwcid="ifLast">
+		<tr class="stack-trace-label">
+			<td colspan="2">Stack Trace:</td>
+		</tr>
+
+		<tr class="stack-trace">
+			<td colspan=2>
+				<ul>
+					<li jwcid="eStack"><span jwcid="insertStackTrace">foo.bar.baz(Line:xyz)</span>
+					</li>
+					<li jwcid="$remove$">foo.bar.baz(Line:xyz)</li>
+					<li jwcid="$remove$">foo.bar.baz(Line:xyz)</li>
+					<li jwcid="$remove$">foo.bar.baz(Line:xyz)</li>					
+					<li jwcid="$remove$">foo.bar.baz(Line:xyz)</li>
+				</ul>
+			</td>
+		</tr>
+</span> <!-- ifLast -->
+</span> <!-- e-exception -->
+
+</table>
+
+</span> <!-- $content$ -->
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/html/ExceptionDisplay.java b/tapestry-framework/src/org/apache/tapestry/html/ExceptionDisplay.java
new file mode 100644
index 0000000..24982d9
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/ExceptionDisplay.java
@@ -0,0 +1,101 @@
+//  Copyright 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.tapestry.html;
+
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.bean.EvenOdd;
+import org.apache.tapestry.util.exception.ExceptionDescription;
+
+/**
+ *  Component used to display an already formatted exception.
+ * 
+ *  [<a href="../../../../../ComponentReference/ExceptionDisplay.html">Component Reference</a>]
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class ExceptionDisplay extends BaseComponent
+{
+    private IBinding _exceptionsBinding;
+    private ExceptionDescription _exception;
+    private int _count;
+    private int _index;
+    private EvenOdd _evenOdd;
+
+    public void setExceptionsBinding(IBinding value)
+    {
+        _exceptionsBinding = value;
+    }
+
+    public IBinding getExceptionsBinding()
+    {
+        return _exceptionsBinding;
+    }
+
+    /**
+     *  Each time the current exception is set, as a side effect,
+     *  the evenOdd helper bean is reset to even.
+     * 
+     **/
+    
+    public void setException(ExceptionDescription value)
+    {
+        _exception = value;
+        
+        _evenOdd.setEven(true);
+    }
+
+    public ExceptionDescription getException()
+    {
+        return _exception;
+    }
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        ExceptionDescription[] exceptions =
+            (ExceptionDescription[]) _exceptionsBinding.getObject(
+                "exceptions",
+                ExceptionDescription[].class);
+
+        _count = exceptions.length;
+        
+        try
+        {
+            _evenOdd = (EvenOdd)getBeans().getBean("evenOdd");
+            
+            super.renderComponent(writer, cycle);
+        }
+        finally
+        {
+            _exception = null;
+            _evenOdd = null;
+        }
+    }
+
+    public void setIndex(int value)
+    {
+        _index = value;
+    }
+
+    public boolean isLast()
+    {
+        return _index == (_count - 1);
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/html/ExceptionDisplay.jwc b/tapestry-framework/src/org/apache/tapestry/html/ExceptionDisplay.jwc
new file mode 100644
index 0000000..c7cf8ae
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/ExceptionDisplay.jwc
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+		
+<component-specification class="org.apache.tapestry.html.ExceptionDisplay" 
+	allow-body="no" 
+	allow-informal-parameters="no">
+  
+  <description>
+  Used to present a detail exception description.
+  </description>
+  
+  <parameter name="exceptions" required="yes">
+    <description>
+    An array of ExceptionDescription objects.
+    </description>
+  </parameter>
+  
+  <bean name="evenOdd" class="org.apache.tapestry.bean.EvenOdd"/>
+    
+  <component id="eException" type="Foreach">
+    <inherited-binding name="source" parameter-name="exceptions"/>
+    <binding name="value" expression="exception"/>
+    <binding name="index" expression="index"/>
+  </component>
+  
+  <component id="insertClass" type="Insert">
+    <binding name="value" expression="exception.exceptionClassName"/>
+  </component>
+  
+  <component id="insertMessage" type="Insert">
+    <binding name="value" expression="exception.message"/>
+  </component>
+  
+  <component id="eProperty" type="Foreach">
+  	<static-binding name="element" value="tr"/>
+  	<binding name="class" expression="beans.evenOdd.next"/>
+    <binding name="source" expression="exception.properties"/>
+  </component>
+  
+  <component id="insertPropertyName" type="Insert">
+    <binding name="value" expression="components.eProperty.value.name"/>
+  </component>
+  
+  <component id="insertPropertyValue" type="Insert">
+    <binding name="value" expression="components.eProperty.value.value"/>
+  </component>
+  
+  <component id="ifLast" type="Conditional">
+    <binding name="condition" expression="last"/>
+  </component>
+  
+  <component id="ifNotLast" type="Conditional">
+    <binding name="condition" expression="! last"/>
+  </component>
+  
+  <component id="eStack" type="Foreach">
+    <static-binding name="element" value="li"/>
+    <binding name="source" expression="exception.stackTrace"/>
+  </component>
+  
+  <component id="insertStackTrace" type="Insert">
+    <binding name="value" expression="components.eStack.value"/>
+  </component>
+  
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/html/Frame.java b/tapestry-framework/src/org/apache/tapestry/html/Frame.java
new file mode 100644
index 0000000..071387f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/Frame.java
@@ -0,0 +1,57 @@
+//  Copyright 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.tapestry.html;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.engine.IEngineService;
+import org.apache.tapestry.engine.ILink;
+
+/**
+ *  Implements a &lt;frame&gt; within a &lt;frameset&gt;.
+ * 
+ *  [<a href="../../../../../ComponentReference/Frame.html">Component Reference</a>]
+ * 
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+public abstract class Frame extends AbstractComponent
+{
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        if (cycle.isRewinding())
+            return;
+
+        IEngine engine = cycle.getEngine();
+        IEngineService pageService = engine.getService(Tapestry.PAGE_SERVICE);
+        ILink link = pageService.getLink(cycle, this, new String[] { getTargetPage() });
+
+        writer.beginEmpty("frame");
+        writer.attribute("src", link.getURL());
+
+        renderInformalParameters(writer, cycle);
+
+        writer.closeTag();
+    }
+
+    public abstract String getTargetPage();
+
+    public abstract void setTargetPage(String targetPage);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/html/Frame.jwc b/tapestry-framework/src/org/apache/tapestry/html/Frame.jwc
new file mode 100644
index 0000000..81ce7bc
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/Frame.jwc
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.html.Frame" 
+	allow-body="no"
+	allow-informal-parameters="yes">
+
+  <description>
+  Identifies a page as the contents of a frame within a frameset.
+  </description>
+
+  <parameter name="page" 
+  		property-name="targetPage"
+  		type="java.lang.String" 
+  		required="yes" 
+  		direction="in">
+    <description>
+	The page to display in the frame.
+    </description>
+  </parameter>
+  
+  <reserved-parameter name="src"/>
+  
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/html/HTMLWriter.java b/tapestry-framework/src/org/apache/tapestry/html/HTMLWriter.java
new file mode 100644
index 0000000..66c010d
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/HTMLWriter.java
@@ -0,0 +1,109 @@
+//  Copyright 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.tapestry.html;
+
+import java.io.OutputStream;
+import java.io.PrintWriter;
+
+import org.apache.tapestry.AbstractMarkupWriter;
+import org.apache.tapestry.IMarkupWriter;
+
+/**
+ *  This class is used to create HTML output.
+ *
+ *  <p>The <code>HTMLWriter</code> handles the necessary escaping 
+ *  of invalid characters.
+ *  Specifically, the '&lt;', '&gt;' and '&amp;' characters are properly
+ *  converted to their HTML entities by the <code>print()</code> methods.
+ *  Similar measures are taken by the {@link #attribute(String, String)} method.
+ *  Other invalid characters are converted to their numeric entity equivalent.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ **/
+
+public class HTMLWriter extends AbstractMarkupWriter
+{
+
+    private static final String[] entities = new String[64];
+    private static final boolean[] safe = new boolean[128];
+
+    private static final String SAFE_CHARACTERS =
+        "01234567890"
+            + "abcdefghijklmnopqrstuvwxyz"
+            + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+            + "\t\n\r !\"#$%'()*+,-./:;=?@[\\]^_`{|}~";
+
+    static {
+        entities['"'] = "&quot;";
+        entities['<'] = "&lt;";
+        entities['>'] = "&gt;";
+        entities['&'] = "&amp;";
+
+        int length = SAFE_CHARACTERS.length();
+        for (int i = 0; i < length; i++)
+            safe[SAFE_CHARACTERS.charAt(i)] = true;
+    }
+
+	/**
+	 *  Creates a new markup writer around the {@link PrintWriter}.
+	 *  The writer will not be closed when the markup writer closes.
+	 *  The content type is currently hard-wired to
+	 *  <code>text/html</code>.
+	 * 
+	 *  @since 3.0
+	 * 
+	 **/
+	
+	public HTMLWriter(PrintWriter writer)
+	{
+		super(safe, entities, "text/html", writer);
+	}
+
+    public HTMLWriter(String contentType, OutputStream outputStream)
+    {
+        super(safe, entities, contentType, outputStream);
+    }
+
+    public HTMLWriter(String contentType, String encoding, OutputStream outputStream)
+    {
+        super(safe, entities, contentType, encoding, outputStream);
+    }
+
+    protected HTMLWriter(String contentType)
+    {
+        super(safe, entities, contentType);
+    }
+
+    /**
+     *  Creates a default writer for content type "text/html; charset=utf-8".
+     * 
+     **/
+
+    public HTMLWriter(OutputStream outputStream)
+    {
+        this(outputStream, "UTF-8");
+    }
+
+    public HTMLWriter(OutputStream outputStream, String encoding)
+    {
+        this("text/html", encoding, outputStream);
+    }
+
+    public IMarkupWriter getNestedWriter()
+    {
+        return new NestedHTMLWriter(this);
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/html/Image.java b/tapestry-framework/src/org/apache/tapestry/html/Image.java
new file mode 100644
index 0000000..0e84cfe
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/Image.java
@@ -0,0 +1,74 @@
+//  Copyright 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.tapestry.html;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.IAsset;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Used to insert an image.  To create a rollover image, use the
+ *  {@link Rollover} class, which integrates a link with the image assets
+ *  used with the button.
+ *
+ *  [<a href="../../../../../ComponentReference/Image.html">Component Reference</a>]
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public abstract class Image extends AbstractComponent
+{
+    /**
+     *  Renders the &lt;img&gt; element.
+     *
+     *
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        // Doesn't contain a body so no need to do anything on rewind (assumes no
+        // sideffects to accessor methods via bindings).
+
+        if (cycle.isRewinding())
+            return;
+
+        IAsset imageAsset = getImage();
+
+        if (imageAsset == null)
+            throw Tapestry.createRequiredParameterException(this, "image");
+
+        String imageURL = imageAsset.buildURL(cycle);
+
+        writer.beginEmpty("img");
+
+        writer.attribute("src", imageURL);
+
+        writer.attribute("border", getBorder());
+
+        renderInformalParameters(writer, cycle);
+
+        writer.closeTag();
+
+    }
+
+    public abstract IAsset getImage();
+
+    public abstract int getBorder();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/html/Image.jwc b/tapestry-framework/src/org/apache/tapestry/html/Image.jwc
new file mode 100644
index 0000000..1728279
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/Image.jwc
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.html.Image" allow-body="no">
+
+  <description>
+  Displays an image, deriving the source URL for the image from an asset.
+  </description>
+
+  <parameter name="image" 
+  	type="org.apache.tapestry.IAsset" 
+  	required="yes"
+  	direction="in">
+    <description>
+    The asset to display.
+    </description>
+  </parameter>
+  
+  <parameter name="border"
+  	type="int"
+  	direction="in">
+    <description>
+    Number of pixels of border.
+    </description>
+  </parameter>
+  
+  <reserved-parameter name="src"/>
+  
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/html/InsertText.java b/tapestry-framework/src/org/apache/tapestry/html/InsertText.java
new file mode 100644
index 0000000..244a7f1
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/InsertText.java
@@ -0,0 +1,129 @@
+//  Copyright 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.tapestry.html;
+
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.io.Reader;
+import java.io.StringReader;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Inserts formatted text (possibly collected using a {@link org.apache.tapestry.form.TextArea} 
+ *  component.
+ * 
+ *  [<a href="../../../../../ComponentReference/InsertText.html">Component Reference</a>]
+ *
+ *  <p>To maintain the line breaks provided originally, this component will
+ *  break the input into individual lines and insert additional
+ *  HTML to make each line seperate.
+ *
+ *  <p>This can be down more simply, using the &lt;pre&gt; HTML element, but
+ *  that usually renders the text in a non-proportional font.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * 
+ **/
+
+public abstract class InsertText extends AbstractComponent
+{
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        String value = getValue();
+
+        if (value == null)
+            return;
+
+        StringReader reader = null;
+        LineNumberReader lineReader = null;
+        InsertTextMode mode = getMode();
+
+        try
+        {
+            reader = new StringReader(value);
+
+            lineReader = new LineNumberReader(reader);
+
+            int lineNumber = 0;
+
+            while (true)
+            {
+                String line = lineReader.readLine();
+
+                // Exit loop at end of file.
+
+                if (line == null)
+                    break;
+
+                mode.writeLine(lineNumber, line, writer);
+
+                lineNumber++;
+            }
+
+        }
+        catch (IOException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("InsertText.conversion-error"),
+                this,
+                null,
+                ex);
+        }
+        finally
+        {
+            close(lineReader);
+            close(reader);
+        }
+
+    }
+
+    private void close(Reader reader)
+    {
+        if (reader == null)
+            return;
+
+        try
+        {
+            reader.close();
+        }
+        catch (IOException e)
+        {
+        }
+    }
+
+    public abstract InsertTextMode getMode();
+
+    public abstract void setMode(InsertTextMode mode);
+
+    public abstract String getValue();
+
+    /**
+     * Sets the mode parameter property to its default,
+     * {@link InsertTextMode#BREAK}.
+     * 
+     * @since 3.0
+     */
+    protected void finishLoad()
+    {
+        setMode(InsertTextMode.BREAK);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/html/InsertText.jwc b/tapestry-framework/src/org/apache/tapestry/html/InsertText.jwc
new file mode 100644
index 0000000..d28af07
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/InsertText.jwc
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.html.InsertText" 
+	allow-body="no" 
+	allow-informal-parameters="no">
+
+  <description>
+  Inserts line-oriented text into the response HTML, inserting additional
+  markup to delimit lines.
+  </description>
+  
+   <parameter name="value" type="java.lang.String" direction="in">
+    <description>
+  	The text to insert.
+    </description>
+  </parameter>
+
+  <parameter name="mode" 
+  	type="org.apache.tapestry.html.InsertTextMode"
+  	direction="in">
+    <description>
+    Determines which mode to use: breaks after each line, or wrap each line
+    as a paragraph.  The default is breaks.
+    </description>
+  </parameter>
+
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/html/InsertTextMode.java b/tapestry-framework/src/org/apache/tapestry/html/InsertTextMode.java
new file mode 100644
index 0000000..ac6789d
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/InsertTextMode.java
@@ -0,0 +1,96 @@
+//  Copyright 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.tapestry.html;
+
+import org.apache.commons.lang.enum.Enum;
+import org.apache.tapestry.IMarkupWriter;
+
+/**
+ *  Defines a number of ways to format multi-line text for proper
+ *  renderring.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public abstract class InsertTextMode extends Enum
+{
+    /**
+     *  Mode where each line (after the first) is preceded by a &lt;br&gt; tag.
+     *
+     **/
+
+    public static final InsertTextMode BREAK = new BreakMode();
+
+    /**
+     *  Mode where each line is wrapped with a &lt;p&gt; element.
+     *
+     **/
+
+    public static final InsertTextMode PARAGRAPH = new ParagraphMode();
+
+    protected InsertTextMode(String name)
+    {
+        super(name);
+    }
+
+    /**
+     *  Invoked by the {@link InsertText} component to write the next line.
+     *
+     *  @param lineNumber the line number of the line, starting with 0 for the first line.
+     *  @param line the String for the current line.
+     *  @param writer the {@link IMarkupWriter} to send output to.
+     **/
+
+    public abstract void writeLine(
+        int lineNumber,
+        String line,
+        IMarkupWriter writer);
+
+    private static class BreakMode extends InsertTextMode
+    {
+        private BreakMode()
+        {
+            super("BREAK");
+        }
+
+        public void writeLine(int lineNumber, String line, IMarkupWriter writer)
+        {
+            if (lineNumber > 0)
+                writer.beginEmpty("br");
+
+            writer.print(line);
+        }
+    }
+
+    private static class ParagraphMode extends InsertTextMode
+    {
+        private ParagraphMode()
+        {
+            super("PARAGRAPH");
+        }
+
+        public void writeLine(int lineNumber, String line, IMarkupWriter writer)
+        {
+            writer.begin("p");
+
+            writer.print(line);
+
+            writer.end();
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/html/NestedHTMLWriter.java b/tapestry-framework/src/org/apache/tapestry/html/NestedHTMLWriter.java
new file mode 100644
index 0000000..a9032ff
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/NestedHTMLWriter.java
@@ -0,0 +1,66 @@
+//  Copyright 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.tapestry.html;
+
+import java.io.CharArrayWriter;
+import java.io.PrintWriter;
+
+import org.apache.tapestry.IMarkupWriter;
+
+/**
+ *  Subclass of {@link HTMLWriter} that is nested.  A nested writer
+ *  buffers its output, then inserts it into its parent writer when it is
+ *  closed.
+ *
+ *  @author Howard Ship
+ *  @version $Id$
+ */
+
+public class NestedHTMLWriter extends HTMLWriter
+{
+	private IMarkupWriter _parent;
+	private CharArrayWriter _internalBuffer;
+
+	public NestedHTMLWriter(IMarkupWriter parent)
+	{
+		super(parent.getContentType());
+
+		_parent = parent;
+
+		_internalBuffer = new CharArrayWriter();
+
+		setWriter(new PrintWriter(_internalBuffer));
+	}
+
+	/**
+	*  Invokes the {@link HTMLWriter#close() super-class
+	*  implementation}, then gets the data accumulated in the
+	*  internal buffer and provides it to the containing writer using
+	*  {@link IMarkupWriter#printRaw(char[], int, int)}.
+	*
+	*/
+
+	public void close()
+	{
+		super.close();
+
+		char[] data = _internalBuffer.toCharArray();
+
+		_parent.printRaw(data, 0, data.length);
+
+		_internalBuffer = null;
+		_parent = null;
+	}
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/html/PracticalBrowserSniffer.js b/tapestry-framework/src/org/apache/tapestry/html/PracticalBrowserSniffer.js
new file mode 100644
index 0000000..95b9360
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/PracticalBrowserSniffer.js
@@ -0,0 +1,165 @@
+// PracticalBrowserSniffer.js - Detect Browser

+// Requires JavaScript 1.1

+/*

+The contents of this file are subject to the Netscape Public

+License Version 1.1 (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.mozilla.org/NPL/

+

+Software distributed under the License is distributed on an "AS

+IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or

+implied. See the License for the specific language governing

+rights and limitations under the License.

+

+The Initial Developer of the Original Code is Bob Clary.

+

+Contributor(s): Bob Clary, Original Work, Copyright 1999-2000

+                Bob Clary, Netscape Communications, Copyright 2001

+

+

+Note:

+

+Acquired from: http://developer.netscape.com/evangelism/tools/practical-browser-sniffing/

+Last update: July 17, 2001

+

+*/

+

+// work around bug in xpcdom Mozilla 0.9.1

+window.saveNavigator = window.navigator;

+

+// Handy functions

+function noop() {}

+function noerror() { return true; }

+

+function defaultOnError(msg, url, line)

+{

+	// customize this for your site

+	if (top.location.href.indexOf('_files/errors/') == -1)

+		top.location = '/evangelism/xbProjects/_files/errors/index.html?msg=' + escape(msg) + '&url=' + escape(url) + '&line=' + escape(line);

+}

+

+// Display Error page... 

+// XXX: more work to be done here

+//

+function reportError(message)

+{

+	// customize this for your site

+	if (top.location.href.indexOf('_files/errors/') == -1)

+		top.location = '/evangelism/xbProjects/_files/errors/index.html?msg=' + escape(message);

+}

+

+function pageRequires(cond, msg, redirectTo)

+{

+	if (!cond)

+	{

+		msg = 'This page requires ' + msg;

+		top.location = redirectTo + '?msg=' + escape(msg);

+	}

+	// return cond so can use in <A> onclick handlers to exclude browsers

+	// from pages they do not support.

+	return cond;

+}

+

+function detectBrowser()

+{

+	var oldOnError = window.onerror;

+	var element = null;

+	

+	window.onerror = defaultOnError;

+

+	navigator.OS		= '';

+	navigator.version	= 0;

+	navigator.org		= '';

+	navigator.family	= '';

+

+	var platform;

+	if (typeof(window.navigator.platform) != 'undefined')

+	{

+		platform = window.navigator.platform.toLowerCase();

+		if (platform.indexOf('win') != -1)

+			navigator.OS = 'win';

+		else if (platform.indexOf('mac') != -1)

+			navigator.OS = 'mac';

+		else if (platform.indexOf('unix') != -1 || platform.indexOf('linux') != -1 || platform.indexOf('sun') != -1)

+			navigator.OS = 'nix';

+	}

+

+	var i = 0;

+	var ua = window.navigator.userAgent.toLowerCase();

+	

+	if (ua.indexOf('safari') != -1) {

+	  navigator.family = 'nn4';

+	  navigator.version = 4;

+	  navigator.org = 'netscape';

+	}

+	else if (ua.indexOf('opera') != -1)

+	{

+		i = ua.indexOf('opera');

+		navigator.family	= 'opera';

+		navigator.org		= 'opera';

+		navigator.version	= parseFloat('0' + ua.substr(i+6), 10);

+	}

+	else if ((i = ua.indexOf('msie')) != -1)

+	{

+		navigator.org		= 'microsoft';

+		navigator.version	= parseFloat('0' + ua.substr(i+5), 10);

+		

+		if (navigator.version < 4)

+			navigator.family = 'ie3';

+		else

+			navigator.family = 'ie4'

+	}

+	else if (typeof(window.controllers) != 'undefined' && typeof(window.locationbar) != 'undefined')

+	{

+		i = ua.lastIndexOf('/')

+		navigator.version = parseFloat('0' + ua.substr(i+1), 10);

+		navigator.family = 'gecko';

+

+		if (ua.indexOf('netscape') != -1)

+			navigator.org = 'netscape';

+		else if (ua.indexOf('compuserve') != -1)

+			navigator.org = 'compuserve';

+		else

+			navigator.org = 'mozilla';

+	}

+	else if ((ua.indexOf('mozilla') !=-1) && (ua.indexOf('spoofer')==-1) && (ua.indexOf('compatible') == -1) && (ua.indexOf('opera')==-1)&& (ua.indexOf('webtv')==-1) && (ua.indexOf('hotjava')==-1))

+	{

+	    var is_major = parseFloat(navigator.appVersion);

+    

+		if (is_major < 4)

+			navigator.version = is_major;

+		else

+		{

+			i = ua.lastIndexOf('/')

+			navigator.version = parseFloat('0' + ua.substr(i+1), 10);

+		}

+		navigator.org = 'netscape';

+		navigator.family = 'nn' + parseInt(navigator.appVersion);

+	}

+	else if ((i = ua.indexOf('aol')) != -1 )

+	{

+		// aol

+		navigator.family	= 'aol';

+		navigator.org		= 'aol';

+		navigator.version	= parseFloat('0' + ua.substr(i+4), 10);

+	}

+

+	navigator.DOMCORE1	= (typeof(document.getElementsByTagName) != 'undefined' && typeof(document.createElement) != 'undefined');

+	navigator.DOMCORE2	= (navigator.DOMCORE1 && typeof(document.getElementById) != 'undefined' && typeof(document.createElementNS) != 'undefined');

+	navigator.DOMHTML	= (navigator.DOMCORE1 && typeof(document.getElementById) != 'undefined');

+	navigator.DOMCSS1	= ( (navigator.family == 'gecko') || (navigator.family == 'ie4') );

+

+	navigator.DOMCSS2   = false;

+	if (navigator.DOMCORE1)

+	{

+		element = document.createElement('p');

+		navigator.DOMCSS2 = (typeof(element.style) == 'object');

+	}

+

+	navigator.DOMEVENTS	= (typeof(document.createEvent) != 'undefined');

+

+	window.onerror = oldOnError;

+}

+

+detectBrowser();

+

diff --git a/tapestry-framework/src/org/apache/tapestry/html/Rollover.java b/tapestry-framework/src/org/apache/tapestry/html/Rollover.java
new file mode 100644
index 0000000..ad9388b
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/Rollover.java
@@ -0,0 +1,197 @@
+//  Copyright 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.tapestry.html;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IAsset;
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.IScript;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.components.ILinkComponent;
+import org.apache.tapestry.components.LinkEventType;
+import org.apache.tapestry.engine.IScriptSource;
+
+/**
+ *  Combines a link component (such as {@link org.apache.tapestry.link.DirectLink}) 
+ *  with an &lt;img&gt; and JavaScript code
+ *  to create a rollover effect that works with both Netscape Navigator and 
+ *  Internet Explorer.
+ *
+ *  [<a href="../../../../../ComponentReference/Rollover.html">Component Reference</a>]
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public abstract class Rollover extends AbstractComponent
+{
+    private IScript _parsedScript;
+
+    /**
+     *  Converts an {@link IAsset} binding into a usable URL.  Returns null
+     *  if the binding does not exist or the binding's value is null.
+     *
+     **/
+
+    protected String getAssetURL(IAsset asset, IRequestCycle cycle)
+    {
+        if (asset == null)
+            return null;
+
+        return asset.buildURL(cycle);
+    }
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        // No body, so we skip it all if not rewinding (assumes no side effects on
+        // accessors).
+
+        if (cycle.isRewinding())
+            return;
+
+        String imageURL = null;
+        String focusURL = null;
+        String blurURL = null;
+        boolean dynamic = false;
+        String imageName = null;
+
+        Body body = Body.get(cycle);
+        if (body == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("Rollover.must-be-contained-by-body"),
+                this,
+                null,
+                null);
+
+        ILinkComponent serviceLink =
+            (ILinkComponent) cycle.getAttribute(Tapestry.LINK_COMPONENT_ATTRIBUTE_NAME);
+
+        if (serviceLink == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("Rollover.must-be-contained-by-link"),
+                this,
+                null,
+                null);
+
+        boolean linkDisabled = serviceLink.isDisabled();
+
+        if (linkDisabled)
+        {
+            imageURL = getAssetURL(getDisabled(), cycle);
+
+            if (imageURL == null)
+                imageURL = getAssetURL(getImage(), cycle);
+        }
+        else
+        {
+            imageURL = getAssetURL(getImage(), cycle);
+            focusURL = getAssetURL(getFocus(), cycle);
+            blurURL = getAssetURL(getBlur(), cycle);
+
+            dynamic = (focusURL != null) || (blurURL != null);
+        }
+
+        if (imageURL == null)
+            throw Tapestry.createRequiredParameterException(this, "image");
+
+        writer.beginEmpty("img");
+
+        writer.attribute("src", imageURL);
+
+        writer.attribute("border", 0);
+
+        if (dynamic)
+        {
+            if (focusURL == null)
+                focusURL = imageURL;
+
+            if (blurURL == null)
+                blurURL = imageURL;
+
+            imageName = writeScript(cycle, body, serviceLink, focusURL, blurURL);
+
+            writer.attribute("name", imageName);
+        }
+
+        renderInformalParameters(writer, cycle);
+
+        writer.closeTag();
+
+    }
+
+    private IScript getParsedScript()
+    {
+        if (_parsedScript == null)
+        {
+            IEngine engine = getPage().getEngine();
+            IScriptSource source = engine.getScriptSource();
+
+            IResourceLocation scriptLocation =
+                getSpecification().getSpecificationLocation().getRelativeLocation(
+                    "Rollover.script");
+
+            _parsedScript = source.getScript(scriptLocation);
+        }
+
+        return _parsedScript;
+    }
+
+    private String writeScript(
+        IRequestCycle cycle,
+        Body body,
+        ILinkComponent link,
+        String focusURL,
+        String blurURL)
+    {
+        String imageName = body.getUniqueString(getId());
+        String focusImageURL = body.getPreloadedImageReference(focusURL);
+        String blurImageURL = body.getPreloadedImageReference(blurURL);
+
+        Map symbols = new HashMap();
+
+        symbols.put("imageName", imageName);
+        symbols.put("focusImageURL", focusImageURL);
+        symbols.put("blurImageURL", blurImageURL);
+
+        getParsedScript().execute(cycle, body, symbols);
+
+        // Add attributes to the link to control mouse over/out.
+        // Because the script is written before the <body> tag,
+        // there won't be any timing issues (such as cause
+        // bug #113893).
+
+        link.addEventHandler(LinkEventType.MOUSE_OVER, (String) symbols.get("onMouseOverName"));
+        link.addEventHandler(LinkEventType.MOUSE_OUT, (String) symbols.get("onMouseOutName"));
+
+        return imageName;
+    }
+
+    public abstract IAsset getBlur();
+
+    public abstract IAsset getDisabled();
+
+    public abstract IAsset getFocus();
+
+    public abstract IAsset getImage();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/html/Rollover.jwc b/tapestry-framework/src/org/apache/tapestry/html/Rollover.jwc
new file mode 100644
index 0000000..f752d7c
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/Rollover.jwc
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.html.Rollover" allow-body="no">
+
+  <description>
+  A complex image component which must be wrapped by a link component.  Rollovers
+  can reflect the enabled status of the link, and display rollover effects.
+  </description>
+
+  <parameter name="image" 
+  	type="org.apache.tapestry.IAsset" 
+  	required="yes"
+  	direction="in">
+    <description>
+    The normal or default image to display, used as a default image for
+    the other parameters.
+    </description>
+  </parameter>
+  
+  <parameter name="focus"
+  	type="org.apache.tapestry.IAsset"
+  	direction="in">
+    <description>
+    If specified, provides an image displayed when the cursor is moved
+    over the link.
+    </description>
+  </parameter>
+  
+  <parameter name="blur" 
+  	type="org.apache.tapestry.IAsset"
+  	direction="in">
+    <description>
+    If specified, provides an image displayed when the cursor is moved
+    off of the link.
+    </description>
+  </parameter>
+  
+  <parameter name="disabled"
+  	type="org.apache.tapestry.IAsset"
+  	direction="in">
+    <description>
+    If specified, provides an image displayed when the surrounding
+    link is disabled.
+    </description>
+  </parameter>
+  
+  <reserved-parameter name="name"/>
+  <reserved-parameter name="src"/>
+  <reserved-parameter name="border"/>
+  
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/html/Rollover.script b/tapestry-framework/src/org/apache/tapestry/html/Rollover.script
new file mode 100644
index 0000000..fd2f641
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/Rollover.script
@@ -0,0 +1,54 @@
+<?xml version="1.0"?>
+<!-- $Id$ -->
+<!DOCTYPE script PUBLIC
+	"-//Apache Software Foundation//Tapestry Script Specification 3.0//EN"
+	"http://jakarta.apache.org/tapestry/dtd/Script_3_0.dtd">
+<script>
+<!-- 
+
+input symbols:
+
+uniqueId - uniqueId used to build names
+focusImageURL - URL for focus (mouse over)
+blurImageURL - URL for blur image (mouse out)
+
+output symbols:
+
+imageName - name for the image (i.e. name attribute of <img> element)
+onMouseOverName - name of mouse over function
+onMouseOutName - name of mouse out function
+
+-->
+
+<input-symbol key="imageName" class="java.lang.String" required="yes"/>
+<input-symbol key="focusImageURL" class="java.lang.String" required="yes"/>
+<input-symbol key="blurImageURL" class="java.lang.String" required="yes"/>
+
+
+<let key="onMouseOverName">
+	focus_${imageName}
+</let>
+<let key="onMouseOutName">
+	blur_${imageName}
+</let>
+
+<let key="attribute">
+	document.${imageName}.src
+</let>
+
+<body>
+
+function ${onMouseOverName}()
+{
+  if (document.images)
+    ${attribute} = ${focusImageURL};
+}
+
+function ${onMouseOutName}()
+{
+  if (document.images)
+    ${attribute} = ${blurImageURL};
+}
+
+</body>
+</script>
diff --git a/tapestry-framework/src/org/apache/tapestry/html/Script.java b/tapestry-framework/src/org/apache/tapestry/html/Script.java
new file mode 100644
index 0000000..1e56f5f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/Script.java
@@ -0,0 +1,185 @@
+//  Copyright 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.tapestry.html;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.IScript;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.engine.IScriptSource;
+
+/**
+ *  Works with the {@link Body} component to add a script (and perhaps some initialization) 
+ *  to the HTML response.
+ *
+ *  [<a href="../../../../../ComponentReference/Script.html">Component Reference</a>]
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+public abstract class Script extends AbstractComponent
+{
+    private Map _baseSymbols;
+
+    /**
+     *  A Map of input and output symbols visible to the body of the Script.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    private Map _symbols;
+
+    /**
+     *  Constructs the symbols {@link Map}.  This starts with the
+     *  contents of the symbols parameter (if specified) to which is added
+     *  any informal parameters.  If both a symbols parameter and informal
+     *  parameters are bound, then a copy of the symbols parameter's value is made
+     *  (that is, the {@link Map} provided by the symbols parameter is read, but not modified).
+     *
+     **/
+
+    private Map getInputSymbols()
+    {
+        Map result = new HashMap();
+
+        if (_baseSymbols != null)
+            result.putAll(_baseSymbols);
+
+        // Now, iterate through all the binding names (which includes both
+        // formal and informal parmeters).  Skip the formal ones and
+        // access the informal ones.
+
+        Iterator i = getBindingNames().iterator();
+        while (i.hasNext())
+        {
+            String bindingName = (String) i.next();
+
+            // Skip formal parameters
+
+            if (getSpecification().getParameter(bindingName) != null)
+                continue;
+
+            IBinding binding = getBinding(bindingName);
+
+            Object value = binding.getObject();
+
+            result.put(bindingName, value);
+        }
+
+        return result;
+    }
+
+    /**
+     *  Gets the {@link IScript} for the correct script.
+     *
+     *
+     **/
+
+    private IScript getParsedScript(IRequestCycle cycle)
+    {
+        String scriptPath = getScriptPath();
+
+        if (scriptPath == null)
+            throw Tapestry.createRequiredParameterException(this, "scriptPath");
+
+        IEngine engine = cycle.getEngine();
+        IScriptSource source = engine.getScriptSource();
+
+        // If the script path is relative, it should be relative to the Script component's
+        // container (i.e., relative to a page in the application).
+
+        IResourceLocation rootLocation =
+            getContainer().getSpecification().getSpecificationLocation();
+        IResourceLocation scriptLocation = rootLocation.getRelativeLocation(scriptPath);
+
+        try
+        {
+            return source.getScript(scriptLocation);
+        }
+        catch (RuntimeException ex)
+        {
+            throw new ApplicationRuntimeException(ex.getMessage(), this, null, ex);
+        }
+
+    }
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        if (!cycle.isRewinding())
+        {
+            Body body = Body.get(cycle);
+
+            if (body == null)
+                throw new ApplicationRuntimeException(
+                    Tapestry.getMessage("Script.must-be-contained-by-body"),
+                    this,
+                    null,
+                    null);
+
+            _symbols = getInputSymbols();
+
+            getParsedScript(cycle).execute(cycle, body, _symbols);
+        }
+
+        // Render the body of the Script;
+        renderBody(writer, cycle);
+    }
+
+    public abstract String getScriptPath();
+
+    public Map getBaseSymbols()
+    {
+        return _baseSymbols;
+    }
+
+    public void setBaseSymbols(Map baseSymbols)
+    {
+        _baseSymbols = baseSymbols;
+    }
+
+    /**
+     *  Returns the complete set of symbols (input and output)
+     *  from the script execution.  This is visible to the body
+     *  of the Script, but is cleared after the Script
+     *  finishes rendering.
+     * 
+     *  @since 2.2
+     **/
+
+    public Map getSymbols()
+    {
+        return _symbols;
+    }
+
+    protected void cleanupAfterRender(IRequestCycle cycle)
+    {
+        _symbols = null;
+
+        super.cleanupAfterRender(cycle);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/html/Script.jwc b/tapestry-framework/src/org/apache/tapestry/html/Script.jwc
new file mode 100644
index 0000000..937850e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/Script.jwc
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.html.Script" allow-body="yes">
+
+  <description>
+  Constructs dynamic JavaScript which is added to the page.
+  </description>
+  
+  <parameter name="script" 
+  	property-name="scriptPath"
+  	type="java.lang.String" 
+  	required="yes" 
+  	direction="in">
+    <description>
+    The resource path of the script to execute.
+    </description>
+  </parameter>
+
+  <parameter name="symbols"
+  	property-name="baseSymbols"
+  	type="java.util.Map" 
+  	direction="in">
+    <description>
+    Provides a base set of symbols to which, in a copy, are added
+    any informal parameters.
+    </description>
+  </parameter>
+
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/html/Shell.java b/tapestry-framework/src/org/apache/tapestry/html/Shell.java
new file mode 100644
index 0000000..eba3eb9
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/Shell.java
@@ -0,0 +1,208 @@
+//  Copyright 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.tapestry.html;
+
+import java.util.Date;
+import java.util.Iterator;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.IAsset;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IPage;
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.engine.IEngineService;
+import org.apache.tapestry.engine.ILink;
+
+/**
+ *  Component for creating a standard 'shell' for a page, which comprises
+ *  the &lt;html&gt; and &lt;head&gt; portions of the page.
+ * 
+ *  [<a href="../../../../../ComponentReference/Shell.html">Component Reference</a>]
+ *
+ *  <p>Specifically does <em>not</em> provide a &lt;body&gt; tag, that is
+ *  usually accomplished using a {@link Body} component.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ * 
+ **/
+
+public abstract class Shell extends AbstractComponent
+{
+
+    private static final String generatorContent =
+        "Tapestry Application Framework, version " + Tapestry.VERSION;
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        long startTime = 0;
+
+        boolean rewinding = cycle.isRewinding();
+
+        if (!rewinding)
+        {
+            startTime = System.currentTimeMillis();
+
+            writeDocType(writer, cycle);
+
+            IPage page = getPage();
+
+            writer.comment("Application: " + page.getEngine().getSpecification().getName());
+
+            writer.comment("Page: " + page.getPageName());
+            writer.comment("Generated: " + new Date());
+
+            writer.begin("html");
+            renderInformalParameters(writer, cycle);
+            writer.println();
+            writer.begin("head");
+            writer.println();
+
+            writer.beginEmpty("meta");
+            writer.attribute("name", "generator");
+            writer.attribute("content", generatorContent);
+            writer.println();
+
+            if (getRenderContentType()) {
+                // This should not be necessary (the HTTP content type should be sufficient), 
+                // but some browsers require it for some reason
+                writer.beginEmpty("meta");
+                writer.attribute("http-equiv", "Content-Type");
+                writer.attribute("content", writer.getContentType());
+                writer.println();
+            }
+
+            writer.begin("title");
+
+            writer.print(getTitle());
+            writer.end(); // title
+            writer.println();
+
+            IRender delegate = getDelegate();
+
+            if (delegate != null)
+                delegate.render(writer, cycle);
+
+            IAsset stylesheet = getStylesheet();
+
+            if (stylesheet != null)
+                writeStylesheetLink(writer, cycle, stylesheet);
+
+            Iterator i = Tapestry.coerceToIterator(getStylesheets());
+
+            if (i != null)
+            {
+                while (i.hasNext())
+                {
+                    stylesheet = (IAsset) i.next();
+
+                    writeStylesheetLink(writer, cycle, stylesheet);
+                }
+            }
+
+            writeRefresh(writer, cycle);
+
+            writer.end(); // head
+        }
+
+        // Render the body, the actual page content
+
+        renderBody(writer, cycle);
+
+        if (!rewinding)
+        {
+            writer.end(); // html
+            writer.println();
+
+            long endTime = System.currentTimeMillis();
+
+            writer.comment("Render time: ~ " + (endTime - startTime) + " ms");
+        }
+
+    }
+
+    private void writeDocType(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        // This code is deprecated and is here only for backward compatibility
+        String DTD = getDTD();
+        if (Tapestry.isNonBlank(DTD)) {
+            writer.printRaw("<!DOCTYPE HTML PUBLIC \"" + DTD + "\">");
+            writer.println();
+            return;
+        }
+
+        // This is the real code
+        String doctype = getDoctype();
+        if (Tapestry.isNonBlank(doctype)) {
+            writer.printRaw("<!DOCTYPE " + doctype + ">");
+            writer.println();
+        }
+    }
+
+    private void writeStylesheetLink(IMarkupWriter writer, IRequestCycle cycle, IAsset stylesheet)
+    {
+        writer.beginEmpty("link");
+        writer.attribute("rel", "stylesheet");
+        writer.attribute("type", "text/css");
+        writer.attribute("href", stylesheet.buildURL(cycle));
+        writer.println();
+    }
+
+    private void writeRefresh(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        int refresh = getRefresh();
+
+        if (refresh <= 0)
+            return;
+
+        // Here comes the tricky part ... have to assemble a complete URL
+        // for the current page.
+
+        IEngineService pageService = cycle.getEngine().getService(Tapestry.PAGE_SERVICE);
+        String pageName = getPage().getPageName();
+
+        ILink link = pageService.getLink(cycle, null, new String[] { pageName });
+
+        StringBuffer buffer = new StringBuffer();
+        buffer.append(refresh);
+        buffer.append("; URL=");
+        buffer.append(link.getAbsoluteURL());
+
+        // Write out the <meta> tag
+
+        writer.beginEmpty("meta");
+        writer.attribute("http-equiv", "Refresh");
+        writer.attribute("content", buffer.toString());
+    }
+
+    public abstract IRender getDelegate();
+
+    public abstract int getRefresh();
+
+    public abstract IAsset getStylesheet();
+
+    public abstract String getTitle();
+
+    public abstract String getDoctype();
+
+    public abstract String getDTD();
+
+    public abstract Object getStylesheets();
+
+    public abstract boolean getRenderContentType();
+    
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/html/Shell.jwc b/tapestry-framework/src/org/apache/tapestry/html/Shell.jwc
new file mode 100644
index 0000000..65b7894
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/Shell.jwc
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.html.Shell" allow-informal-parameters="yes">
+
+  <description>
+  Provides the outer tags in an HTML page: &lt;html&gt;, &lt;head&gt; and &lt;title&gt;.
+  </description>
+
+  <parameter name="title" 
+  	type="java.lang.String" 
+  	required="yes"
+  	direction="in">
+    <description>
+    The title for the page.
+    </description>
+  </parameter>
+  
+  <parameter name="stylesheet" 
+  	type="org.apache.tapestry.IAsset"
+  	direction="in">
+    <description>
+    If specified, provides an external stylesheet for the page.
+    </description>
+  </parameter>
+  
+  <parameter name="stylesheets" type="java.lang.Object" direction="in">
+  	<description>
+  	Array or collection of stylesheet assets.
+  	</description>	
+  </parameter>
+
+  <parameter name="doctype" 
+  	type="java.lang.String"
+  	direction="in"
+  	default-value='"HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\""'>
+    <description>
+    Used to specify the full definition of the DOCTYPE element in the response page,
+    for example 'math SYSTEM "http://www.w3.org/Math/DTD/mathml1/mathml.dtd"' 
+
+    The list of currently valid DOCTYPE settings can be found here:
+    http://www.w3.org/QA/2002/04/valid-dtd-list.html
+
+    If the parameter is null or empty, no DOCTYPE tag will be rendered
+    </description>
+  </parameter>
+
+  <parameter name="DTD" 
+  	type="java.lang.String"
+  	direction="in"
+  	default-value='null'>
+    <description>
+    This parameter is deprecated. Please use the 'doctype' parameter instead.
+
+    Used to specify the DOCTYPE DTD of the response page.
+    </description>
+  </parameter>
+  
+  <parameter 
+  	name="renderContentType" 
+  	type="boolean"
+  	direction="in"
+  	default-value="true">
+    <description>
+    Determines whether to render an http-equiv element with the Content Type of this response.
+    </description>
+  </parameter>
+  
+  <parameter 
+  	name="refresh" 
+  	type="int"
+  	direction="in">
+    <description>
+    If specified, the page will refresh itself after the specified delay (in seconds).
+    </description>
+  </parameter>
+  
+  <parameter 
+  	name="delegate" 
+  	type="org.apache.tapestry.IRender"
+  	direction="in">
+    <description>
+    If specified, the delegate is rendered before the close of the &lt;head&gt;
+    tag (typically used to provide &lt;meta&gt; tags).
+    </description>
+  </parameter>
+  
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/html/package.html b/tapestry-framework/src/org/apache/tapestry/html/package.html
new file mode 100644
index 0000000..a74d010
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/html/package.html
@@ -0,0 +1,15 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+<p>Components specific to the creation of HTML pages, including sophisticated
+DHTML JavaScript effects.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/jsp/AbstractLinkTag.java b/tapestry-framework/src/org/apache/tapestry/jsp/AbstractLinkTag.java
new file mode 100644
index 0000000..dfda6d6
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/jsp/AbstractLinkTag.java
@@ -0,0 +1,119 @@
+//  Copyright 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.tapestry.jsp;
+
+import java.io.IOException;
+
+import javax.servlet.jsp.JspException;
+import javax.servlet.jsp.JspWriter;
+
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Abstract super-class of Tapestry JSP tags that produce a hyperlink
+ *  (<code>&lt;a&gt;</code>) tag.  Tags use a
+ *  {@link org.apache.tapestry.jsp.URLRetriever} for the <code>href</code>
+ *  attribute, and may include a 
+ *  <code>class</code> attribute (based on
+ *  the {@link #getStyleClass() styleClass property}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public abstract class AbstractLinkTag extends AbstractTapestryTag
+{
+    private String _styleClass;
+
+    public String getStyleClass()
+    {
+        return _styleClass;
+    }
+
+    public void setStyleClass(String styleClass)
+    {
+        _styleClass = styleClass;
+    }
+
+    /**
+    *  Writes a <code>&lt;/a&gt;</code> tag.
+    * 
+    *  @return {@link javax.servlet.jsp.tagext.Tag#EVAL_PAGE}
+    * 
+    **/
+
+    public int doEndTag() throws JspException
+    {
+        JspWriter out = pageContext.getOut();
+
+        try
+        {
+            out.print("</a>");
+        }
+        catch (IOException ex)
+        {
+            throw new JspException(
+                Tapestry.format("AbstractLinkTag.io-exception", ex.getMessage()));
+        }
+
+        return EVAL_PAGE;
+    }
+
+    /**
+    *  Writes a <code>&lt;a&gt; tag.  The tag may
+    *  have a <code>class</code> attribute if the
+    *  {@link #getStyleClass() styleClass property}
+    *  is not null.  The <code>href</code>
+    *  attribute is provided via
+    *  a {@link #getURLRetriever() URLRetriever}.
+    * 
+    *  @return {@link javax.servlet.jsp.tagext.Tag#EVAL_BODY_INCLUDE}
+    * 
+    **/
+
+    public int doStartTag() throws JspException
+    {
+        JspWriter out = pageContext.getOut();
+
+        try
+        {
+            out.print("<a");
+
+            if (_styleClass != null)
+            {
+                out.print(" class=\"");
+                out.print(_styleClass);
+                out.print('"');
+            }
+
+            out.print(" href=\"");
+
+            getURLRetriever().insertURL(getServlet());
+
+            // And we're back!  Finish off the tag.
+
+            out.print("\">");
+        }
+        catch (IOException ex)
+        {
+            throw new JspException(
+                Tapestry.format("AbstractLinkTag.io-exception", ex.getMessage()));
+        }
+
+        return EVAL_BODY_INCLUDE;
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/jsp/AbstractTapestryTag.java b/tapestry-framework/src/org/apache/tapestry/jsp/AbstractTapestryTag.java
new file mode 100644
index 0000000..8bbed55
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/jsp/AbstractTapestryTag.java
@@ -0,0 +1,163 @@
+//  Copyright 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.tapestry.jsp;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.jsp.JspException;
+import javax.servlet.jsp.tagext.TagSupport;
+
+import ognl.ClassResolver;
+import ognl.DefaultClassResolver;
+
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.link.DirectLink;
+import org.apache.tapestry.parse.TemplateParser;
+import org.apache.tapestry.util.prop.OgnlUtils;
+
+/**
+ *  Contains common code and methods for all the Tapestry JSP tag implementations.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public abstract class AbstractTapestryTag extends TagSupport
+{
+
+    private String _servlet = "/app";
+
+    public String getServlet()
+    {
+        return _servlet;
+    }
+
+    public void setServlet(String servlet)
+    {
+        _servlet = servlet;
+    }
+
+    /**
+     *  Implemented in subclasses to provide a 
+     *  {@link org.apache.tapestry.jsp.URLRetriever} instance that
+     *  can insert the correct URL into the output.
+     * 
+     **/
+
+    protected abstract URLRetriever getURLRetriever() throws JspException;
+
+    /**
+     *  Builds an object array appropriate for use as the service parameters
+     *  for the external service. The first object in the array is the name
+     *  of the page.  Any additional objects are service parameters to be
+     *  supplied to the listener method.
+     * 
+     *  <p>
+     *  The parameters are converted to an array of objects
+     *  via {@link #convertParameters(String)}.
+     * 
+     **/
+
+    protected Object[] constructExternalServiceParameters(String pageName, String parameters)
+        throws JspException
+    {
+
+        Object[] resolvedParameters = convertParameters(parameters);
+
+        int count = Tapestry.size(resolvedParameters);
+
+        if (count == 0)
+            return new Object[] { pageName };
+
+        List list = new ArrayList();
+        list.add(pageName);
+
+        for (int i = 0; i < count; i++)
+            list.add(resolvedParameters[i]);
+
+        return list.toArray();
+    }
+
+    /**
+     *  <p>The external service allows service parameters (an array of
+     *  objects) to be passed along inside the URL.  This method converts
+     *  the input string into an array of parameter objects.
+     *  <ul>
+     *  <li>If parameters is null, the no parameters are passed
+     *  <li>If parameters starts with "ognl:" it is treated as an OGNL expression:
+     *     <ul>
+     *     <li>The expression is evaluated using the 
+     *         {@link javax.servlet.jsp.PageContext page context} as the root object
+     *     <li>If the expression value is a Map, then the Map is converted to
+     *  	   an array via (@link org.apache.tapestry.Tapestry#convertMapToArray(Map)}
+     *     <li>Otherwise, the expression value is converted using
+     *  {@link org.apache.tapestry.link.DirectLink#constructServiceParameters(Object)}.
+     * 		</ul>
+     *   <li>Otherwise, parameters are simply a string, which is included as the lone
+     *  service parameter
+     *  </ul>
+     **/
+
+    protected Object[] convertParameters(String _parameters) throws JspException
+    {
+        if (_parameters == null)
+            return null;
+
+        if (_parameters.startsWith(TemplateParser.OGNL_EXPRESSION_PREFIX))
+        {
+            String expression =
+                _parameters.substring(TemplateParser.OGNL_EXPRESSION_PREFIX.length() + 1);
+
+            return convertExpression(expression);
+        }
+
+        return new Object[] { _parameters };
+    }
+
+    private Object[] convertExpression(String expression) throws JspException
+    {
+        Object value = evaluateExpression(expression);
+
+        if (value == null)
+            return null;
+
+        if (value instanceof Map)
+            return Tapestry.convertMapToArray((Map) value);
+
+        return DirectLink.constructServiceParameters(value);
+    }
+
+    private Object evaluateExpression(String expression) throws JspException
+    {
+        ClassResolver resolver = new DefaultClassResolver();
+
+        try
+        {
+            return OgnlUtils.get(expression, resolver, pageContext);
+        }
+        catch (Throwable t)
+        {
+            throw new JspException(
+                Tapestry.format(
+                    "AbstractTapestryTag.unable-to-evaluate-expression",
+                    expression,
+                    t.getMessage()));
+        }
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/jsp/AbstractURLTag.java b/tapestry-framework/src/org/apache/tapestry/jsp/AbstractURLTag.java
new file mode 100644
index 0000000..f516312
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/jsp/AbstractURLTag.java
@@ -0,0 +1,42 @@
+//  Copyright 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.tapestry.jsp;
+
+import javax.servlet.jsp.JspException;
+
+/**
+ *  Base class for tags which simply insert a URL into the output.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public abstract class AbstractURLTag extends AbstractTapestryTag
+{
+
+    /**
+	 *  Inserts the URL and returns {@link javax.servlet.jsp.tagext.Tag#SKIP_BODY}.
+	 * 
+	 **/
+	
+    public int doStartTag() throws JspException
+    {
+        getURLRetriever().insertURL(getServlet());
+        
+        return SKIP_BODY;
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/jsp/ExternalTag.java b/tapestry-framework/src/org/apache/tapestry/jsp/ExternalTag.java
new file mode 100644
index 0000000..6dd80e4
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/jsp/ExternalTag.java
@@ -0,0 +1,63 @@
+//  Copyright 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.tapestry.jsp;
+
+import javax.servlet.jsp.JspException;
+
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  JSP tag that makes use of the Tapestry external service.
+ *  Parameters may be passed in the URL.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class ExternalTag extends AbstractLinkTag
+{
+    private String _parameters;
+    private String _page;
+
+    protected URLRetriever getURLRetriever() throws JspException
+    {
+        return new URLRetriever(pageContext, Tapestry.EXTERNAL_SERVICE, 
+        constructExternalServiceParameters(_page, _parameters));
+    }
+
+
+    public void setParameters(String parameters)
+    {
+        _parameters = parameters;
+    }
+
+    public String getPage()
+    {
+        return _page;
+    }
+
+    public void setPage(String page)
+    {
+        _page = page;
+    }
+
+    public String getParameters()
+    {
+        return _parameters;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/jsp/ExternalURLTag.java b/tapestry-framework/src/org/apache/tapestry/jsp/ExternalURLTag.java
new file mode 100644
index 0000000..9999453
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/jsp/ExternalURLTag.java
@@ -0,0 +1,64 @@
+//  Copyright 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.tapestry.jsp;
+
+import javax.servlet.jsp.JspException;
+
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Like the {@link org.apache.tapestry.jsp.ExternalTag}, but inserts just
+ *  the URL.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+public class ExternalURLTag extends AbstractURLTag
+{
+
+    private String _parameters;
+    private String _page;
+
+    protected URLRetriever getURLRetriever() throws JspException
+    {
+        return new URLRetriever(
+            pageContext,
+            Tapestry.EXTERNAL_SERVICE,
+            constructExternalServiceParameters(_page, _parameters));
+    }
+
+    public void setParameters(String parameters)
+    {
+        _parameters = parameters;
+    }
+
+    public String getPage()
+    {
+        return _page;
+    }
+
+    public void setPage(String page)
+    {
+        _page = page;
+    }
+
+    public String getParameters()
+    {
+        return _parameters;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/jsp/PageTag.java b/tapestry-framework/src/org/apache/tapestry/jsp/PageTag.java
new file mode 100644
index 0000000..5e587ad
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/jsp/PageTag.java
@@ -0,0 +1,47 @@
+//  Copyright 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.tapestry.jsp;
+
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Creates a link from a JSP page to a Tapestry application page.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class PageTag extends AbstractLinkTag
+{
+    private String _page;
+
+    public String getPage()
+    {
+        return _page;
+    }
+
+    public void setPage(String pageName)
+    {
+        _page = pageName;
+    }
+
+    protected URLRetriever getURLRetriever()
+    {
+        return new URLRetriever(pageContext, Tapestry.PAGE_SERVICE, new String[] { _page });
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/jsp/PageURLTag.java b/tapestry-framework/src/org/apache/tapestry/jsp/PageURLTag.java
new file mode 100644
index 0000000..721820f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/jsp/PageURLTag.java
@@ -0,0 +1,49 @@
+//  Copyright 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.tapestry.jsp;
+
+import javax.servlet.jsp.JspException;
+
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Inserts just the URL for a page service request into the output.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class PageURLTag extends AbstractURLTag
+{
+	private String _page;	
+
+    protected URLRetriever getURLRetriever() throws JspException
+    {
+        return new URLRetriever(pageContext, Tapestry.PAGE_SERVICE, new String[] { _page });
+    }
+
+    public String getPage()
+    {
+        return _page;
+    }
+
+    public void setPage(String page)
+    {
+        _page = page;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/jsp/URLRetriever.java b/tapestry-framework/src/org/apache/tapestry/jsp/URLRetriever.java
new file mode 100644
index 0000000..ad45431
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/jsp/URLRetriever.java
@@ -0,0 +1,102 @@
+//  Copyright 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.tapestry.jsp;
+
+import java.io.IOException;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.jsp.JspException;
+import javax.servlet.jsp.PageContext;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Encapsulates the process of calling into the Tapestry servlet to retrieve
+ *  a URL.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+public class URLRetriever
+{
+    private static final Log LOG = LogFactory.getLog(URLRetriever.class);
+
+    private PageContext _pageContext;
+    private String _serviceName;
+    private Object[] _serviceParameters;
+
+    public URLRetriever(PageContext pageContext, String serviceName, Object[] serviceParameters)
+    {
+        _pageContext = pageContext;
+        _serviceName = serviceName;
+        _serviceParameters = serviceParameters;
+    }
+
+    /**
+     *  Invokes the servlet to retrieve the URL.  The URL is inserted
+     *  into the output (as with
+     *  {@link RequestDispatcher#include(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}). 
+     * 
+     *
+     **/
+
+    public void insertURL(String servletPath) throws JspException
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Obtaining Tapestry URL for service " + _serviceName + " at " + servletPath);
+
+        ServletRequest request = _pageContext.getRequest();
+
+        RequestDispatcher dispatcher = request.getRequestDispatcher(servletPath);
+
+        if (dispatcher == null)
+            throw new JspException(
+                Tapestry.format("URLRetriever.unable-to-find-dispatcher", servletPath));
+
+        request.setAttribute(Tapestry.TAG_SUPPORT_SERVICE_ATTRIBUTE, _serviceName);
+        request.setAttribute(Tapestry.TAG_SUPPORT_PARAMETERS_ATTRIBUTE, _serviceParameters);
+        request.setAttribute(Tapestry.TAG_SUPPORT_SERVLET_PATH_ATTRIBUTE, servletPath);
+
+        try
+        {
+            _pageContext.getOut().flush();
+
+            dispatcher.include(request, _pageContext.getResponse());
+        }
+        catch (IOException ex)
+        {
+            throw new JspException(
+                Tapestry.format("URLRetriever.io-exception", servletPath, ex.getMessage()));
+        }
+        catch (ServletException ex)
+        {
+            throw new JspException(
+                Tapestry.format("URLRetriever.servlet-exception", servletPath, ex.getMessage()));
+        }
+        finally
+        {
+            request.removeAttribute(Tapestry.TAG_SUPPORT_SERVICE_ATTRIBUTE);
+            request.removeAttribute(Tapestry.TAG_SUPPORT_PARAMETERS_ATTRIBUTE);
+            request.removeAttribute(Tapestry.TAG_SUPPORT_SERVLET_PATH_ATTRIBUTE);
+        }
+
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/jsp/package.html b/tapestry-framework/src/org/apache/tapestry/jsp/package.html
new file mode 100644
index 0000000..bd9219b
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/jsp/package.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+
+<html>
+<body>
+
+A simple JSP tag library that allows JSPs to include links to a Tapestry application.
+Currently supports just the page and external services.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/link/AbsoluteLinkRenderer.java b/tapestry-framework/src/org/apache/tapestry/link/AbsoluteLinkRenderer.java
new file mode 100644
index 0000000..709ee1c
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/link/AbsoluteLinkRenderer.java
@@ -0,0 +1,94 @@
+//  Copyright 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.tapestry.link;
+
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.engine.ILink;
+
+/**
+ *  Renders a link using an absolute URL, not simply a URI
+ *  (as with {@link org.apache.tapestry.link.DefaultLinkRenderer}.  In addition,
+ *  the scheme, server and port may be changed (this may be appropriate when
+ *  switching between secure and insecure portions of an application).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ * 
+ **/
+
+public class AbsoluteLinkRenderer extends DefaultLinkRenderer
+{
+    private String _scheme;
+    private String _serverName;
+    private int _port;
+
+    public int getPort()
+    {
+        return _port;
+    }
+
+    public String getScheme()
+    {
+        return _scheme;
+    }
+
+    public String getServerName()
+    {
+        return _serverName;
+    }
+
+    /**
+     *  Used to override the port in the final URL, if specified.  If not specified,
+     *  the port provided by the {@link javax.servlet.ServletRequest#getServerPort() request}
+     *  is used (typically, the value 80).
+     *
+     **/
+
+    public void setPort(int port)
+    {
+        _port = port;
+    }
+    
+    /**
+     *  Used to override the scheme in the final URL, if specified.  If not specified,
+     *  the scheme provided by the {@link javax.servlet.ServletRequest#getScheme() request}
+     *  is used (typically, <code>http</code>).
+     *
+     **/
+
+    public void setScheme(String scheme)
+    {
+        _scheme = scheme;
+    }
+
+    /**
+     *  Used to override the server name in the final URL, if specified.  If not specified,
+     *  the port provided by the {@link javax.servlet.ServletRequest#getServerName() request}
+     *  is used.
+     *
+     **/
+
+    public void setServerName(String serverName)
+    {
+        _serverName = serverName;
+    }
+
+    protected String constructURL(ILink link, String anchor, IRequestCycle cycle)
+    {
+        return link.getAbsoluteURL(_scheme, _serverName, _port, anchor, true);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/link/AbstractLinkComponent.java b/tapestry-framework/src/org/apache/tapestry/link/AbstractLinkComponent.java
new file mode 100644
index 0000000..ccbb76e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/link/AbstractLinkComponent.java
@@ -0,0 +1,234 @@
+//  Copyright 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.tapestry.link;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.components.ILinkComponent;
+import org.apache.tapestry.components.LinkEventType;
+import org.apache.tapestry.engine.IEngineService;
+import org.apache.tapestry.engine.ILink;
+import org.apache.tapestry.html.Body;
+
+/**
+ *  Base class for
+ *  implementations of {@link ILinkComponent}.  Includes a disabled attribute
+ *  (that should be bound to a disabled parameter), 
+ *  an anchor attribute, and a
+ *  renderer attribute (that should be bound to a renderer parameter).  A default,
+ *  shared instance of {@link org.apache.tapestry.link.DefaultLinkRenderer} is
+ *  used when no specific renderer is provided.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+public abstract class AbstractLinkComponent extends AbstractComponent implements ILinkComponent
+{
+    private Map _eventHandlers;
+
+    public abstract boolean isDisabled();
+
+    /**
+     *  Adds an event handler (typically, from a wrapped component such
+     *  as a {@link org.apache.tapestry.html.Rollover}).
+     *
+     **/
+
+    public void addEventHandler(LinkEventType eventType, String functionName)
+    {
+        Object currentValue;
+
+        if (_eventHandlers == null)
+            _eventHandlers = new HashMap();
+
+        currentValue = _eventHandlers.get(eventType);
+
+        // The first value is added as a String
+
+        if (currentValue == null)
+        {
+            _eventHandlers.put(eventType, functionName);
+            return;
+        }
+
+        // When adding the second value, convert to a List
+
+        if (currentValue instanceof String)
+        {
+            List list = new ArrayList();
+            list.add(currentValue);
+            list.add(functionName);
+
+            _eventHandlers.put(eventType, list);
+            return;
+        }
+
+        // For the third and up, add the new function to the List
+
+        List list = (List) currentValue;
+        list.add(functionName);
+    }
+
+    /**
+     *  Renders the link by delegating to an instance
+     *  of {@link ILinkRenderer}.
+     *
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        getRenderer().renderLink(writer, cycle, this);
+    }
+
+    protected void cleanupAfterRender(IRequestCycle cycle)
+    {
+        _eventHandlers = null;
+
+        super.cleanupAfterRender(cycle);
+    }
+
+    protected void writeEventHandlers(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        String name = null;
+
+        if (_eventHandlers == null)
+            return;
+
+        Body body = Body.get(cycle);
+
+        if (body == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("AbstractLinkComponent.events-need-body"),
+                this,
+                null,
+                null);
+
+        Iterator i = _eventHandlers.entrySet().iterator();
+
+        while (i.hasNext())
+        {
+            Map.Entry entry = (Map.Entry) i.next();
+            LinkEventType type = (LinkEventType) entry.getKey();
+
+            name = writeEventHandler(writer, body, name, type.getAttributeName(), entry.getValue());
+        }
+
+    }
+
+    protected String writeEventHandler(
+        IMarkupWriter writer,
+        Body body,
+        String name,
+        String attributeName,
+        Object value)
+    {
+        String wrapperFunctionName;
+
+        if (value instanceof String)
+        {
+            wrapperFunctionName = (String) value;
+        }
+        else
+        {
+            String finalName = name == null ? body.getUniqueString("Link") : name;
+
+            wrapperFunctionName = attributeName + "_" + finalName;
+
+            StringBuffer buffer = new StringBuffer();
+
+            buffer.append("function ");
+            buffer.append(wrapperFunctionName);
+            buffer.append(" ()\n{\n");
+
+            Iterator i = ((List) value).iterator();
+            while (i.hasNext())
+            {
+                String functionName = (String) i.next();
+                buffer.append("  ");
+                buffer.append(functionName);
+                buffer.append("();\n");
+            }
+
+            buffer.append("}\n\n");
+
+            body.addBodyScript(buffer.toString());
+        }
+
+        writer.attribute(attributeName, "javascript:" + wrapperFunctionName + "();");
+
+        return name;
+    }
+
+    /** @since 3.0 **/
+
+    public abstract ILinkRenderer getRenderer();
+
+    public abstract void setRenderer(ILinkRenderer renderer);
+
+    public void renderAdditionalAttributes(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        writeEventHandlers(writer, cycle);
+
+        // Generate additional attributes from informal parameters.
+
+        renderInformalParameters(writer, cycle);
+    }
+
+    /**
+     *  Utility method for subclasses; Gets the named service from the engine
+     *  and invokes {@link IEngineService#getLink(IRequestCycle, org.apache.tapestry.IComponent, Object[])}
+     *  on it.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    protected ILink getLink(IRequestCycle cycle, String serviceName, Object[] serviceParameters)
+    {
+        IEngineService service = cycle.getEngine().getService(serviceName);
+
+        return service.getLink(cycle, this, serviceParameters);
+    }
+
+    public abstract String getAnchor();
+
+    public ILink getLink(IRequestCycle cycle)
+    {
+        return null;
+    }
+
+    /**
+     * Sets the renderer parameter property to its default value
+     * {@link DefaultLinkRenderer#SHARED_INSTANCE}.
+     * 
+     * @since 3.0
+     */
+    protected void finishLoad()
+    {
+        setRenderer(DefaultLinkRenderer.SHARED_INSTANCE);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/link/ActionLink.java b/tapestry-framework/src/org/apache/tapestry/link/ActionLink.java
new file mode 100644
index 0000000..fa01d01
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/link/ActionLink.java
@@ -0,0 +1,77 @@
+//  Copyright 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.tapestry.link;
+
+import org.apache.tapestry.IAction;
+import org.apache.tapestry.IActionListener;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.RenderRewoundException;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.engine.ILink;
+
+/**
+ *  A component for creating a link that is handled using the action service.
+ * 
+ *  [<a href="../../../../../ComponentReference/ActionLink.html">Component Reference</a>]
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+public abstract class ActionLink extends AbstractLinkComponent implements IAction
+{
+    /**
+     *  Returns true if the stateful parameter is bound to
+     *  a true value.  If stateful is not bound, also returns
+     *  the default, true.
+     * 
+     *  <p>Note that this method can be called when the
+     *  component is not rendering, therefore it must
+     *  directly access the {@link IBinding} for the stateful
+     *  parameter.
+     *
+     **/
+
+    public boolean getRequiresSession()
+    {
+    	IBinding statefulBinding = getStatefulBinding();
+    	
+        if (statefulBinding == null)
+            return true;
+
+        return statefulBinding.getBoolean();
+    }
+
+    public ILink getLink(IRequestCycle cycle)
+    {
+        String actionId = cycle.getNextActionId();
+
+        if (cycle.isRewound(this))
+        {
+            getListener().actionTriggered(this, cycle);
+
+            throw new RenderRewoundException(this);
+        }
+
+        return getLink(cycle, Tapestry.ACTION_SERVICE, new Object[] { actionId });
+    }
+
+    public abstract IBinding getStatefulBinding();
+
+    public abstract IActionListener getListener();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/link/ActionLink.jwc b/tapestry-framework/src/org/apache/tapestry/link/ActionLink.jwc
new file mode 100644
index 0000000..658ad5e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/link/ActionLink.jwc
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.link.ActionLink">
+
+  <description>
+  Creates a contextual link within the response page.  The page will be rewound to its
+  original state before the listener is invoked.
+  </description>
+  
+  <parameter name="listener" 
+  	type="org.apache.tapestry.IActionListener" 
+  	required="yes"
+  	direction="in"/>
+  	
+  <parameter name="disabled"
+  	type="boolean"
+  	direction="in"/>
+  	  	
+  <parameter name="anchor" 
+  	type="java.lang.String"
+  	direction="in"/>
+  	
+  <parameter name="renderer"
+  	type="org.apache.tapestry.link.ILinkRenderer"
+  	direction="in"/>
+  	
+  <parameter name="stateful" type="boolean" direction="custom" property-name="stateful"/>	
+  
+  <reserved-parameter name="href"/>
+  
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/link/DefaultLinkRenderer.java b/tapestry-framework/src/org/apache/tapestry/link/DefaultLinkRenderer.java
new file mode 100644
index 0000000..4a3ceb6
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/link/DefaultLinkRenderer.java
@@ -0,0 +1,162 @@
+//  Copyright 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.tapestry.link;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.components.ILinkComponent;
+import org.apache.tapestry.engine.ILink;
+
+/**
+ *  Default implementation of {@link org.apache.tapestry.link.ILinkRenderer}, which
+ *  does nothing special.  Can be used as a base class to provide
+ *  additional handling.
+ *
+ *  @author Howard Lewis Ship, David Solis
+ *  @version $Id$
+ *  @since 3.0
+ **/
+
+public class DefaultLinkRenderer implements ILinkRenderer
+{
+    /**
+     *  A shared instance used as a default for any link that doesn't explicitly
+     *  override.
+     * 
+     **/
+
+    public static final ILinkRenderer SHARED_INSTANCE = new DefaultLinkRenderer();
+
+    public void renderLink(IMarkupWriter writer, IRequestCycle cycle, ILinkComponent linkComponent)
+    {
+        IMarkupWriter wrappedWriter = null;
+
+        if (cycle.getAttribute(Tapestry.LINK_COMPONENT_ATTRIBUTE_NAME) != null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("AbstractLinkComponent.no-nesting"),
+                linkComponent,
+                null,
+                null);
+
+        cycle.setAttribute(Tapestry.LINK_COMPONENT_ATTRIBUTE_NAME, linkComponent);
+
+        boolean hasBody = getHasBody();
+
+        boolean disabled = linkComponent.isDisabled();
+
+        if (!disabled)
+        {
+            ILink l = linkComponent.getLink(cycle);
+
+            if (hasBody)
+                writer.begin(getElement());
+            else
+                writer.beginEmpty(getElement());
+
+            writer.attribute(getUrlAttribute(), constructURL(l, linkComponent.getAnchor(), cycle));
+
+            beforeBodyRender(writer, cycle, linkComponent);
+
+            // Allow the wrapped components a chance to render.
+            // Along the way, they may interact with this component
+            // and cause the name variable to get set.
+
+            wrappedWriter = writer.getNestedWriter();
+        }
+        else
+            wrappedWriter = writer;
+
+        if (hasBody)
+            linkComponent.renderBody(wrappedWriter, cycle);
+
+        if (!disabled)
+        {
+            afterBodyRender(writer, cycle, linkComponent);
+
+            linkComponent.renderAdditionalAttributes(writer, cycle);
+
+            if (hasBody)
+            {
+                wrappedWriter.close();
+
+                // Close the <element> tag
+
+                writer.end();
+            }
+            else
+                writer.closeTag();
+        }
+
+        cycle.removeAttribute(Tapestry.LINK_COMPONENT_ATTRIBUTE_NAME);
+    }
+
+    /**
+     *  Converts the EngineServiceLink into a URI or URL.  This implementation
+     *  simply invokes {@link ILink#getURL(String, boolean)}.
+     * 
+     **/
+
+    protected String constructURL(ILink link, String anchor, IRequestCycle cycle)
+    {
+        return link.getURL(anchor, true);
+    }
+
+    /**
+     *  Invoked after the href attribute has been written but before
+     *  the body of the link is rendered (but only if the link
+     *  is not disabled).
+     * 
+     *  <p>
+     *  This implementation does nothing.
+     * 
+     **/
+
+    protected void beforeBodyRender(IMarkupWriter writer, IRequestCycle cycle, ILinkComponent link)
+    {
+    }
+
+    /**
+     *  Invoked after the body of the link is rendered, but before
+     *  {@link ILinkComponent#renderAdditionalAttributes(IMarkupWriter, IRequestCycle)} is invoked
+     *  (but only if the link is not disabled).
+     * 
+     *  <p>
+     *  This implementation does nothing.
+     * 
+     **/
+
+    protected void afterBodyRender(IMarkupWriter writer, IRequestCycle cycle, ILinkComponent link)
+    {
+    }
+
+    /** @since 3.0 **/
+
+    protected String getElement()
+    {
+        return "a";
+    }
+
+    protected String getUrlAttribute()
+    {
+        return "href";
+    }
+
+    protected boolean getHasBody()
+    {
+        return true;
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/link/DirectLink.java b/tapestry-framework/src/org/apache/tapestry/link/DirectLink.java
new file mode 100644
index 0000000..2c7ff20
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/link/DirectLink.java
@@ -0,0 +1,125 @@
+//  Copyright 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.tapestry.link;
+
+import java.util.List;
+
+import org.apache.tapestry.IActionListener;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IDirect;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.engine.ILink;
+
+/**
+ *  A component for creating a link using the direct service; used for actions that
+ *  are not dependant on dynamic page state.
+ *
+ *  [<a href="../../../../../ComponentReference/DirectLink.html">Component Reference</a>]
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ *
+ **/
+
+public abstract class DirectLink extends AbstractLinkComponent implements IDirect
+{
+
+    public abstract IBinding getStatefulBinding();
+    public abstract IActionListener getListener();
+
+    /**
+     *  Returns true if the stateful parameter is bound to
+     *  a true value.  If stateful is not bound, also returns
+     *  the default, true.  May be invoked when not renderring.
+     *
+     **/
+
+    public boolean isStateful()
+    {
+        IBinding statefulBinding = getStatefulBinding();
+
+        if (statefulBinding == null)
+            return true;
+
+        return statefulBinding.getBoolean();
+    }
+
+    public ILink getLink(IRequestCycle cycle)
+    {
+        return getLink(cycle, Tapestry.DIRECT_SERVICE, constructServiceParameters(getParameters()));
+    }
+
+    /**
+     *  Converts a service parameters value to an array
+     *  of objects.  
+     *  This is used by the {@link DirectLink}, {@link ServiceLink}
+     *  and {@link ExternalLink}
+     *  components.
+     *
+     *  @param parameterValue the input value which may be
+     *  <ul>
+     *  <li>null  (returns null)
+     *  <li>An array of Object (returns the array)
+     *  <li>A {@link List} (returns an array of the values in the List})
+     *  <li>A single object (returns the object as a single-element array)
+     *  </ul>
+     * 
+     *  @return An array representation of the input object.
+     * 
+     *  @since 2.2
+     **/
+
+    public static Object[] constructServiceParameters(Object parameterValue)
+    {
+        if (parameterValue == null)
+            return null;
+
+        if (parameterValue instanceof Object[])
+            return (Object[]) parameterValue;
+
+        if (parameterValue instanceof List)
+        {
+            List list = (List) parameterValue;
+
+            return list.toArray();
+        }
+
+        return new Object[] { parameterValue };
+    }
+
+    /**
+     *  Invoked by the direct service to trigger the application-specific
+     *  action by notifying the {@link IActionListener listener}.
+     *
+     *  @throws org.apache.tapestry.StaleSessionException if the component is stateful, and
+     *  the session is new.
+     * 
+     **/
+
+    public void trigger(IRequestCycle cycle)
+    {
+        IActionListener listener = getListener();
+        
+        if (listener == null)
+        	throw Tapestry.createRequiredParameterException(this, "listener");
+
+        listener.actionTriggered(this, cycle);
+    }
+
+    /** @since 2.2 **/
+
+    public abstract Object getParameters();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/link/DirectLink.jwc b/tapestry-framework/src/org/apache/tapestry/link/DirectLink.jwc
new file mode 100644
index 0000000..e3a214e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/link/DirectLink.jwc
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.link.DirectLink">
+
+  <description>
+  Creates a non-contextual link.  Non-persistent state can be stored within the link
+  using the context.
+  </description>
+
+  <parameter name="listener" 
+  	type="org.apache.tapestry.IActionListener" 
+  	required="yes"
+  	direction="auto"/>
+  	
+  <parameter name="parameters" type="java.lang.Object" direction="in">
+    <description>
+    An object, or list of objects, encoded into the URL
+    as service parameters.
+    </description>
+  </parameter>
+     
+  <parameter name="stateful" type="boolean" direction="custom"/>
+  
+  <parameter name="disabled"
+  	type="boolean"
+  	direction="in"/>
+  	  	
+  <parameter name="anchor" 
+  	type="java.lang.String"
+  	direction="in"/>
+  	
+  <parameter name="renderer"
+  	type="org.apache.tapestry.link.ILinkRenderer"
+  	direction="in"/>
+  
+  <reserved-parameter name="href"/>
+    
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/link/ExternalLink.java b/tapestry-framework/src/org/apache/tapestry/link/ExternalLink.java
new file mode 100644
index 0000000..a6e6958
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/link/ExternalLink.java
@@ -0,0 +1,62 @@
+//  Copyright 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.tapestry.link;
+
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.engine.ILink;
+
+/**
+ *  A component for creating a link to {@link org.apache.tapestry.IExternalPage} using the
+ * {@link org.apache.tapestry.engine.ExternalService}.
+ *
+ *  [<a href="../../../../../ComponentReference/ExternalLink.html">Component Reference</a>]
+ *
+ * @see org.apache.tapestry.IExternalPage
+ * @see org.apache.tapestry.engine.ExternalService
+ *
+ * @author Malcolm Edgar
+ * @version $Id$
+ *
+ **/
+
+public abstract class ExternalLink extends AbstractLinkComponent
+{
+    public ILink getLink(IRequestCycle cycle)
+    {
+        return getLink(cycle, Tapestry.EXTERNAL_SERVICE, getServiceParameters());
+    }
+
+    private Object[] getServiceParameters()
+    {
+        Object[] pageParameters = DirectLink.constructServiceParameters(getParameters());
+        String targetPage = getTargetPage();
+
+        if (pageParameters == null)
+            return new Object[] { targetPage };
+
+        Object[] parameters = new Object[pageParameters.length + 1];
+
+        parameters[0] = targetPage;
+
+        System.arraycopy(pageParameters, 0, parameters, 1, pageParameters.length);
+
+        return parameters;
+    }
+
+    public abstract Object getParameters();
+
+    public abstract String getTargetPage();
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/link/ExternalLink.jwc b/tapestry-framework/src/org/apache/tapestry/link/ExternalLink.jwc
new file mode 100644
index 0000000..c1b31e5
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/link/ExternalLink.jwc
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.link.ExternalLink">
+
+  <description>Creates a IExternalPage link.</description>
+
+  <parameter name="page" 
+  	type="java.lang.String" 
+  	required="yes" 
+  	property-name="targetPage"
+  	direction="in"/>
+
+  <parameter name="parameters" type="java.lang.Object" direction="in">
+    <description>
+    An object, or list of objects, encoded into the URL
+    as service parameters.
+    </description>
+  </parameter>
+  
+  <parameter name="disabled"
+  	type="boolean"
+  	direction="in"/>
+  	  	
+  <parameter name="anchor" 
+  	type="java.lang.String"
+  	direction="in"/>
+  	
+  <parameter name="renderer"
+  	type="org.apache.tapestry.link.ILinkRenderer"
+  	direction="in"/>
+  	
+  <reserved-parameter name="href"/>
+    
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/link/GenericLink.java b/tapestry-framework/src/org/apache/tapestry/link/GenericLink.java
new file mode 100644
index 0000000..7c42f6a
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/link/GenericLink.java
@@ -0,0 +1,43 @@
+//  Copyright 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.tapestry.link;
+
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.engine.ILink;
+
+/**
+ *  An implementation of {@link org.apache.tapestry.components.ILinkComponent} 
+ *  that allows
+ *  the exact HREF to be specified, usually used for client side
+ *  scripting.  
+ * 
+ *  [<a href="../../../../../ComponentReference/GenericLink.html">Component Reference</a>]
+ * 
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.0.2
+ * 
+ **/
+
+public abstract class GenericLink extends AbstractLinkComponent
+{
+    public abstract String getHref();
+
+    public ILink getLink(IRequestCycle cycle)
+    {
+        return new StaticLink(getHref());
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/link/GenericLink.jwc b/tapestry-framework/src/org/apache/tapestry/link/GenericLink.jwc
new file mode 100644
index 0000000..2e33130
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/link/GenericLink.jwc
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.link.GenericLink">
+
+  <description>
+  Creates a link to an application specified URL.
+  </description>
+  
+  <parameter name="href" type="java.lang.String" required="yes" direction="in"/>
+
+  <parameter name="disabled"
+  	type="boolean"
+  	direction="in"/>
+  	  	
+  <parameter name="anchor" 
+  	type="java.lang.String"
+  	direction="in"/>
+  	
+  <parameter name="renderer"
+  	type="org.apache.tapestry.link.ILinkRenderer"
+  	direction="in"/>
+  
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/link/ILinkRenderer.java b/tapestry-framework/src/org/apache/tapestry/link/ILinkRenderer.java
new file mode 100644
index 0000000..14d1309
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/link/ILinkRenderer.java
@@ -0,0 +1,54 @@
+//  Copyright 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.tapestry.link;
+
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.components.ILinkComponent;
+
+/**
+ *  Used by various instances of {@link org.apache.tapestry.components.ILinkComponent} to
+ *  actually renderer a link.  Implementations of the interface can manipulate
+ *  some of the details of how the link is written.
+ * 
+ *  <p>
+ *  A link rendered may be used in many threads, and must be threadsafe.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ * 
+ **/
+
+public interface ILinkRenderer
+{
+    /**
+     *  Renders the link, taking into account whether the link is
+     *  {@link org.apache.tapestry.components.ILinkComponent#isDisabled() disabled}.
+     *  This is complicated by the fact that the rendering of the body must be done
+     *  within a nested writer, since the Link component will not render its tag
+     *  until after its body renders (to allow for any wrapped components that need
+     *  to write event handlers for the link).
+     * 
+     *  <p>
+     *  The renderer is expected to call back into the link component to handle
+     *  any informal parameters, and to handle events output.
+     * 
+     * 
+     **/
+
+    public void renderLink(IMarkupWriter writer, IRequestCycle cycle, ILinkComponent linkComponent);
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/link/PageLink.java b/tapestry-framework/src/org/apache/tapestry/link/PageLink.java
new file mode 100644
index 0000000..9e76785
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/link/PageLink.java
@@ -0,0 +1,54 @@
+//  Copyright 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.tapestry.link;
+
+import org.apache.tapestry.INamespace;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.engine.ILink;
+
+/**
+ *  A component for creating a navigation link to another page, 
+ *  using the page service.
+ *
+ *  [<a href="../../../../../ComponentReference/PageLink.html">Component Reference</a>]
+ *
+ * @author Howard Ship
+ * @version $Id$
+ *
+ **/
+
+public abstract class PageLink extends AbstractLinkComponent
+{
+    public ILink getLink(IRequestCycle cycle)
+    {
+        String parameter = null;
+        INamespace namespace = getTargetNamespace();
+        String targetPage = getTargetPage();
+
+        if (namespace == null)
+            parameter = targetPage;
+        else
+            parameter = namespace.constructQualifiedName(targetPage);
+
+        return getLink(cycle, Tapestry.PAGE_SERVICE, new String[] { parameter });
+    }
+
+    public abstract String getTargetPage();
+
+    /** @since 2.2 **/
+
+    public abstract INamespace getTargetNamespace();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/link/PageLink.jwc b/tapestry-framework/src/org/apache/tapestry/link/PageLink.jwc
new file mode 100644
index 0000000..7a07237
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/link/PageLink.jwc
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.link.PageLink">
+
+  <description>
+  Creates a link to another page within the application.
+  </description>
+  
+  <parameter name="page" 
+  	type="java.lang.String" 
+  	required="yes" 
+  	property-name="targetPage"
+  	direction="in"/>
+  
+  <parameter name="namespace" 
+  	type="org.apache.tapestry.INamespace" 
+  	required="no" 
+  	property-name="targetNamespace"
+  	direction="in"/>
+  	  	
+  <parameter name="disabled"
+  	type="boolean"
+  	direction="in"/>
+  	  	
+  <parameter name="anchor" 
+  	type="java.lang.String"
+  	direction="in"/>
+  	
+  <parameter name="renderer"
+  	type="org.apache.tapestry.link.ILinkRenderer"
+  	direction="in"/>
+  
+  <reserved-parameter name="href"/>
+
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/link/ServiceLink.java b/tapestry-framework/src/org/apache/tapestry/link/ServiceLink.java
new file mode 100644
index 0000000..46378a5
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/link/ServiceLink.java
@@ -0,0 +1,45 @@
+//  Copyright 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.tapestry.link;
+
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.engine.ILink;
+
+/**
+ *  A component for creating a link for an arbitrary {@link org.apache.tapestry.engine.IEngineService
+ *  engine service}.  A ServiceLink component can emulate an {@link ActionLink},
+ *  {@link PageLink} or {@link DirectLink} component, but is most often used in
+ *  conjunction with an application-specific service.  
+ *
+ *  [<a href="../../../../../ComponentReference/ServiceLink.html">Component Reference</a>]
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+public abstract class ServiceLink extends AbstractLinkComponent
+{
+    public ILink getLink(IRequestCycle cycle)
+    {
+        Object[] parameters = DirectLink.constructServiceParameters(getParameters());
+
+        return getLink(cycle, getService(), parameters);
+    }
+
+    public abstract String getService();
+
+    public abstract Object getParameters();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/link/ServiceLink.jwc b/tapestry-framework/src/org/apache/tapestry/link/ServiceLink.jwc
new file mode 100644
index 0000000..66c2047
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/link/ServiceLink.jwc
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.link.ServiceLink">
+
+  <description>
+  Creates a link using an arbitrary engine service.
+  </description>
+  
+  <parameter name="service" type="java.lang.String" required="yes" direction="in"/>
+
+  <parameter name="parameters" type="java.lang.Object" direction="in">
+    <description>
+    A object or string, or array of objects and strings, encoded into the URL
+    as service parameters.
+    </description>
+  </parameter>
+
+  <parameter name="disabled"
+  	type="boolean"
+  	direction="in"/>
+  	  	
+  <parameter name="anchor" 
+  	type="java.lang.String"
+  	direction="in"/>
+  	
+  <parameter name="renderer"
+  	type="org.apache.tapestry.link.ILinkRenderer"
+  	direction="in"/>
+
+  <reserved-parameter name="href"/>
+  
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/link/StaticLink.java b/tapestry-framework/src/org/apache/tapestry/link/StaticLink.java
new file mode 100644
index 0000000..7ee0836
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/link/StaticLink.java
@@ -0,0 +1,75 @@
+//  Copyright 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.tapestry.link;
+
+import org.apache.tapestry.engine.ILink;
+
+/**
+ *  Used by {@link org.apache.tapestry.link.GenericLink} to represent
+ *  an external, static URL.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ * 
+ **/
+public class StaticLink implements ILink
+{
+	private String _url;
+
+	public StaticLink(String url)
+	{
+		_url = url;
+	}
+
+    public String getURL()
+    {
+        return _url;
+    }
+
+    public String getURL(String anchor, boolean includeParameters)
+    {
+        if (anchor == null)
+        	return _url;
+        	
+        	return _url + "#" + anchor;
+    }
+
+    public String getAbsoluteURL()
+    {
+        return _url;
+    }
+
+    public String getAbsoluteURL(
+        String scheme,
+        String server,
+        int port,
+        String anchor,
+        boolean includeParameters)
+    {
+        return getURL(anchor, false);
+    }
+
+    public String[] getParameterNames()
+    {
+        return null;
+    }
+
+    public String[] getParameterValues(String name)
+    {
+        throw new IllegalArgumentException();
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/link/package.html b/tapestry-framework/src/org/apache/tapestry/link/package.html
new file mode 100644
index 0000000..4c99d7f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/link/package.html
@@ -0,0 +1,26 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+<p>Components for creating links on the page that trigger application behavior
+when clicked.  These links are compatible with HTML and WML (they
+use the basic &lt;a&gt; element).
+
+<p>Each component is related to a different 
+{@link org.apache.tapestry.engine.IEngineService}, except for {@link org.apache.tapestry.link.ServiceLink}
+which is parameterized to use any of the available services ... which is useful with
+applications that define their own services.
+
+<p>Link components have a second function, to provide event handling support to the 
+components they wrap.  This is how a {@link org.apache.tapestry.html.Rollover} component manages
+to include DHTML and JavaScript that preloads the images and changes the displayed image as the
+mouse enters and exits the "hot" area defined by the link.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/listener/ListenerMap.java b/tapestry-framework/src/org/apache/tapestry/listener/ListenerMap.java
new file mode 100644
index 0000000..4258188
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/listener/ListenerMap.java
@@ -0,0 +1,319 @@
+//  Copyright 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.tapestry.listener;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import ognl.OgnlRuntime;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IActionListener;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Maps a class to a set of listeners based on the public methods of the class.
+ *  {@link org.apache.tapestry.listener.ListenerMapPropertyAccessor} is setup
+ *  to provide these methods as named properties of the ListenerMap.
+ *
+ *  @author Howard Ship
+ *  @version $Id$
+ *  @since 1.0.2
+ * 
+ **/
+
+public class ListenerMap
+{
+    private static final Log LOG = LogFactory.getLog(ListenerMap.class);
+
+    static {
+        OgnlRuntime.setPropertyAccessor(ListenerMap.class, new ListenerMapPropertyAccessor());
+    }
+
+    private Object _target;
+
+    /**
+     *  A {@link Map} of relevant {@link Method}s, keyed on method name.
+     *  This is just the public void methods that take an {@link IRequestCycle}
+     *  and throw nothing or just {@link ApplicationRuntimeException}.
+     */
+
+    private Map _methodMap;
+
+    /**
+     * A {@link Map} of cached listener instances, keyed on method name
+     *
+     **/
+
+    private Map _listenerCache = new HashMap();
+
+    /**
+     * A {@link Map}, keyed on Class, of {@link Map} ... the method map
+     * for any particular instance of the given class.
+     *
+     **/
+
+    private static Map _classMap = new HashMap();
+
+    /**
+     *  Implements both listener interfaces.
+     *
+     **/
+
+    private class SyntheticListener implements IActionListener
+    {
+        private Method _method;
+
+        SyntheticListener(Method method)
+        {
+            _method = method;
+        }
+
+        private void invoke(IRequestCycle cycle)
+        {
+            Object[] args = new Object[] { cycle };
+
+            invokeTargetMethod(_target, _method, args);
+        }
+
+        public void actionTriggered(IComponent component, IRequestCycle cycle)
+        {
+            invoke(cycle);
+        }
+
+        public String toString()
+        {
+            StringBuffer buffer = new StringBuffer("SyntheticListener[");
+
+            buffer.append(_target);
+            buffer.append(' ');
+            buffer.append(_method);
+            buffer.append(']');
+
+            return buffer.toString();
+        }
+
+    }
+
+    public ListenerMap(Object target)
+    {
+        _target = target;
+    }
+
+    /**
+     *  Gets a listener for the given name (which is both a property name
+     *  and a method name).  The listener is created as needed, but is
+     *  also cached for later use.
+     *
+     * @throws ApplicationRuntimeException if the listener can not be created.
+     **/
+
+    public synchronized Object getListener(String name)
+    {
+        Object listener = null;
+
+        listener = _listenerCache.get(name);
+
+        if (listener == null)
+        {
+            listener = createListener(name);
+
+            _listenerCache.put(name, listener);
+        }
+
+        return listener;
+    }
+
+    /**
+     *  Returns an object that implements {@link IActionListener}.
+     *  This involves looking up the method by name and determining which
+     *  inner class to create.
+     **/
+
+    private synchronized Object createListener(String name)
+    {
+        if (_methodMap == null)
+            getMethodMap();
+
+        Method method = (Method) _methodMap.get(name);
+
+        if (method == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.format("ListenerMap.object-missing-method", _target, name));
+
+        return new SyntheticListener(method);
+    }
+
+    /**
+     *  Gets the method map for the current instance.  If necessary, it is constructed and cached (for other instances
+     *  of the same class).
+     *
+     **/
+
+    private synchronized Map getMethodMap()
+    {
+        if (_methodMap != null)
+            return _methodMap;
+
+        Class beanClass = _target.getClass();
+
+        synchronized (_classMap)
+        {
+            _methodMap = (Map) _classMap.get(beanClass);
+
+            if (_methodMap == null)
+            {
+                _methodMap = buildMethodMap(beanClass);
+
+                _classMap.put(beanClass, _methodMap);
+            }
+        }
+
+        return _methodMap;
+    }
+
+    private static Map buildMethodMap(Class beanClass)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Building method map for class " + beanClass.getName());
+
+        Map result = new HashMap();
+        Method[] methods = beanClass.getMethods();
+
+        for (int i = 0; i < methods.length; i++)
+        {
+            Method m = methods[i];
+            int mods = m.getModifiers();
+
+            if (Modifier.isStatic(mods))
+                continue;
+
+            // Probably not necessary, getMethods() returns only public
+            // methods.
+
+            if (!Modifier.isPublic(mods))
+                continue;
+
+            // Must return void
+
+            if (m.getReturnType() != Void.TYPE)
+                continue;
+
+            Class[] parmTypes = m.getParameterTypes();
+
+            if (parmTypes.length != 1)
+                continue;
+
+            // parm must be IRequestCycle
+
+            if (!parmTypes[0].equals(IRequestCycle.class))
+                continue;
+
+            // Ha!  Passed all tests.
+
+            result.put(m.getName(), m);
+        }
+
+        return result;
+
+    }
+
+    /**
+     *  Invoked by the inner listener/adaptor classes to
+     *  invoke the method.
+     *
+     **/
+
+    private static void invokeTargetMethod(Object target, Method method, Object[] args)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Invoking listener method " + method + " on " + target);
+
+        try
+        {
+            try
+            {
+                method.invoke(target, args);
+            }
+            catch (InvocationTargetException ex)
+            {
+                Throwable inner = ex.getTargetException();
+
+                if (inner instanceof ApplicationRuntimeException)
+                    throw (ApplicationRuntimeException) inner;
+
+                // Edit out the InvocationTargetException, if possible.
+
+                if (inner instanceof RuntimeException)
+                    throw (RuntimeException) inner;
+
+                throw ex;
+            }
+        }
+        catch (ApplicationRuntimeException ex)
+        {
+            throw ex;
+        }
+        catch (Exception ex)
+        {
+            // Catch InvocationTargetException or, preferrably,
+            // the inner exception here (if its a runtime exception).
+
+            throw new ApplicationRuntimeException(
+                Tapestry.format("ListenerMap.unable-to-invoke-method", method.getName(), target, ex.getMessage()),
+                ex);
+        }
+    }
+
+    /** 
+     *  Returns an unmodifiable collection of the 
+     *  names of the listeners implemented by the target class.
+     *
+     *  @since 1.0.6
+     *
+     **/
+
+    public synchronized Collection getListenerNames()
+    {
+         return Collections.unmodifiableCollection(getMethodMap().keySet());
+    }
+
+    /**
+     *  Returns true if this ListenerMap can provide a listener
+     *  with the given name.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public synchronized boolean canProvideListener(String name)
+    {
+         return getMethodMap().containsKey(name);
+    }
+
+    public String toString()
+    {
+        return "ListenerMap[" + _target + "]";
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/listener/ListenerMapPropertyAccessor.java b/tapestry-framework/src/org/apache/tapestry/listener/ListenerMapPropertyAccessor.java
new file mode 100644
index 0000000..2d58bcf
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/listener/ListenerMapPropertyAccessor.java
@@ -0,0 +1,70 @@
+//  Copyright 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.tapestry.listener;
+
+import java.util.Map;
+
+import ognl.ObjectPropertyAccessor;
+import ognl.OgnlException;
+
+/**
+ *  Exposes {@link org.apache.tapestry.IActionListener} listeners
+ *  provided by the {@link ListenerMap} as read-only properties
+ *  of the ListenerMap.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.2
+ *
+ **/
+
+public class ListenerMapPropertyAccessor extends ObjectPropertyAccessor
+{
+    /**
+     *  Checks to see if the ListenerMap provides the named
+     *  listener, returning the listener if it does.  Otherwise,
+     *  invokes the super implementation.
+     * 
+     **/
+    
+    public Object getProperty(Map context, Object target, Object name) throws OgnlException
+    {
+        ListenerMap map = (ListenerMap) target;
+        String listenerName = (String) name;
+
+        if (map.canProvideListener(listenerName))
+            return map.getListener(listenerName);
+
+        return super.getProperty(context, target, name);
+    }
+    
+    /**
+     *  Returns true if the ListenerMap contains the named listener,
+     *  otherwise invokes super-implementation.
+     * 
+     **/
+
+    public boolean hasGetProperty(Map context, Object target, Object oname) throws OgnlException
+    {
+        ListenerMap map = (ListenerMap) target;
+        String listenerName = (String) oname;
+
+        if (map.canProvideListener(listenerName))
+            return true;
+
+        return super.hasGetProperty(context, target, oname);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/listener/package.html b/tapestry-framework/src/org/apache/tapestry/listener/package.html
new file mode 100644
index 0000000..9b10c07
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/listener/package.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+
+<html>
+<body>
+
+<p>Support classes that allows an object
+to expose listener <em>methods</em> instead of listener <em>properties</em>.
+
+<p>
+Normally, a listener property must be an object that implement
+{@link org.apache.tapestry.IActionListener}.  This can be cumbersome, in practice, as it
+typically involves creating an anonymous inner class.
+
+<p>
+Using this mechanism, classes can instead implement listener <em>methods</em>.
+A listener method takes the form:
+
+<pre>
+public void <em>method-name</em>({@link org.apache.tapestry.IRequestCycle} cycle) 
+throws {@link org.apache.tapestry.ApplicationRuntimeException}</code>
+</pre>
+
+<p>The <code>throws</code> clause is optional, but may not throw any 
+additional exceptions.
+
+<p>Tapestry will create an appropriate listener object that will invoke the
+corresponding method.
+
+<p>The methods can be accessed using the property path "<code>listeners.<em>method-name</em></code>"
+
+@see org.apache.tapestry.listener.ListenerMap
+@see org.apache.tapestry.AbstractComponent#getListeners()
+@see org.apache.tapestry.engine.AbstractEngine#getListeners()
+@since 1.0.2
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/multipart/DefaultMultipartDecoder.java b/tapestry-framework/src/org/apache/tapestry/multipart/DefaultMultipartDecoder.java
new file mode 100644
index 0000000..ce3f327
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/multipart/DefaultMultipartDecoder.java
@@ -0,0 +1,247 @@
+//  Copyright 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.tapestry.multipart;
+
+import java.io.UnsupportedEncodingException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.fileupload.DiskFileUpload;
+import org.apache.commons.fileupload.FileItem;
+import org.apache.commons.fileupload.FileUpload;
+import org.apache.commons.fileupload.FileUploadException;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.request.IUploadFile;
+
+/**
+ *  Decodes the data in a <code>multipart/form-data</code> HTTP request, handling
+ *  file uploads and multi-valued parameters.  After decoding, the class is used
+ *  to access the parameter values.
+ * 
+ *  <p>This implementation is a thin wrapper around the Apache Jakarta
+ *  <a href="http://jakarta.apache.org/commons/fileupload/">FileUpload</a>. 
+ *  
+ *  <p>Supports single valued parameters, multi-valued parameters and individual
+ *  file uploads.  That is, for file uploads, each upload must be a unique parameter
+ *  (that is all the {@link org.apache.tapestry.form.Upload} component needs).
+
+ *
+ *  @author Joe Panico
+ *  @version $Id$
+ *  @since 2.0.1
+ *
+ **/
+public class DefaultMultipartDecoder implements IMultipartDecoder
+{
+    /**
+     *  Request attribute key used to store the part map for this request.
+     *  The part map is created in {@link #decode(HttpServletRequest)}.  By storing
+     *  the part map in the request instead of an instance variable, DefaultMultipartDecoder
+     *  becomes threadsafe (no client-specific state in instance variables).
+     * 
+     **/
+
+    public static final String PART_MAP_ATTRIBUTE_NAME = "org.apache.tapestry.multipart.part-map";
+
+    private int _maxSize = 10000000;
+    private int _thresholdSize = 1024;
+    private String _repositoryPath = System.getProperty("java.io.tmpdir");
+
+    private static DefaultMultipartDecoder _shared;
+
+    public static DefaultMultipartDecoder getSharedInstance()
+    {
+        if (_shared == null)
+            _shared = new DefaultMultipartDecoder();
+
+        return _shared;
+    }
+
+    public void setMaxSize(int maxSize)
+    {
+        _maxSize = maxSize;
+    }
+
+    public int getMaxSize()
+    {
+        return _maxSize;
+    }
+
+    public void setThresholdSize(int thresholdSize)
+    {
+        _thresholdSize = thresholdSize;
+    }
+
+    public int getThresholdSize()
+    {
+        return _thresholdSize;
+    }
+
+    public void setRepositoryPath(String repositoryPath)
+    {
+        _repositoryPath = repositoryPath;
+    }
+
+    public String getRepositoryPath()
+    {
+        return _repositoryPath;
+    }
+
+    public static boolean isMultipartRequest(HttpServletRequest request)
+    {
+        return FileUpload.isMultipartContent(request);
+    }
+
+    /**
+     *  Invokes {@link IPart#cleanup()} on each part.
+     * 
+     **/
+    public void cleanup(HttpServletRequest request)
+    {
+        Map partMap = getPartMap(request);
+
+        Iterator i = partMap.values().iterator();
+        while (i.hasNext())
+        {
+            IPart part = (IPart) i.next();
+            part.cleanup();
+        }
+    }
+
+    /**
+     * Decodes the request, storing the part map (keyed on query parameter name, 
+     * value is {@link IPart} into the request as an attribute.
+     * 
+     * @throws ApplicationRuntimeException if decode fails, for instance the
+     * request exceeds getMaxSize()
+     * 
+     **/
+
+    public void decode(HttpServletRequest request)
+    {
+        Map partMap = new HashMap();
+
+        request.setAttribute(PART_MAP_ATTRIBUTE_NAME, partMap);
+
+        // The encoding that will be used to decode the string parameters
+        // It should NOT be null at this point, but it may be 
+        // if the older Servlet API 2.2 is used
+        String encoding = request.getCharacterEncoding();
+
+        // DiskFileUpload is not quite threadsafe, so we create a new instance
+        // for each request.
+
+        DiskFileUpload upload = new DiskFileUpload();
+
+        List parts = null;
+
+        try
+        {
+            if (encoding != null)
+                upload.setHeaderEncoding(encoding);
+            parts = upload.parseRequest(request, _thresholdSize, _maxSize, _repositoryPath);
+        }
+        catch (FileUploadException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("DefaultMultipartDecoder.unable-to-decode", ex.getMessage()),
+                ex);
+        }
+
+        int count = Tapestry.size(parts);
+
+        for (int i = 0; i < count; i++)
+        {
+            FileItem uploadItem = (FileItem) parts.get(i);
+
+            if (uploadItem.isFormField())
+            {
+                try
+                {
+                    String name = uploadItem.getFieldName();
+                    String value;
+                    if (encoding == null)
+                        value = uploadItem.getString();
+                    else
+                        value = uploadItem.getString(encoding);
+                        
+                    ValuePart valuePart = (ValuePart) partMap.get(name);
+                    if (valuePart != null)
+                    {
+                        valuePart.add(value);
+                    }
+                    else
+                    {
+                        valuePart = new ValuePart(value);
+                        partMap.put(name, valuePart);
+                    }
+                }
+                catch (UnsupportedEncodingException ex)
+                {
+                    throw new ApplicationRuntimeException(
+                        Tapestry.format("illegal-encoding", encoding),
+                        ex);
+                }
+            }
+            else
+            {
+                UploadPart uploadPart = new UploadPart(uploadItem);
+
+                partMap.put(uploadItem.getFieldName(), uploadPart);
+            }
+        }
+
+    }
+
+    public String getString(HttpServletRequest request, String name)
+    {
+        Map partMap = getPartMap(request);
+
+        ValuePart part = (ValuePart) partMap.get(name);
+        if (part != null)
+            return part.getValue();
+
+        return null;
+    }
+
+    public String[] getStrings(HttpServletRequest request, String name)
+    {
+        Map partMap = getPartMap(request);
+
+        ValuePart part = (ValuePart) partMap.get(name);
+        if (part != null)
+            return part.getValues();
+
+        return null;
+    }
+
+    public IUploadFile getUploadFile(HttpServletRequest request, String name)
+    {
+        Map partMap = getPartMap(request);
+
+        return (IUploadFile) partMap.get(name);
+    }
+
+    private Map getPartMap(HttpServletRequest request)
+    {
+        return (Map) request.getAttribute(PART_MAP_ATTRIBUTE_NAME);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/multipart/IMultipartDecoder.java b/tapestry-framework/src/org/apache/tapestry/multipart/IMultipartDecoder.java
new file mode 100644
index 0000000..b9ebbd5
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/multipart/IMultipartDecoder.java
@@ -0,0 +1,78 @@
+//  Copyright 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.tapestry.multipart;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.tapestry.request.IUploadFile;
+
+/**
+ *  Defines how a multipart HTTP request can be broken into individual
+ *  elements (including file uploads).
+ * 
+ *  <p>Multipart decoder implementations must be threadsafe.
+ *  
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.3
+ **/
+
+public interface IMultipartDecoder
+{
+    /**
+     *  Decodes the incoming request, identifying all
+     *  the parts (values and uploaded files) contained
+     *  within.
+     * 
+     **/
+
+    public void decode(HttpServletRequest request);
+
+    /**
+     *  Invoked to release any resources needed by tghe
+     *  decoder.  In some cases, large incoming parts
+     *  are written to temporary files; this method
+     *  ensures those temporary files are deleted.
+     * 
+     **/
+
+    public void cleanup(HttpServletRequest request);
+
+    /**
+     *  Returns the single value (or first value) for the parameter
+     *  with the specified name.  Returns null if no such parameter
+     *  was in the request.
+     * 
+     **/
+
+    public String getString(HttpServletRequest request, String name);
+
+    /**
+     *  Returns an array of values (possibly a single element array).
+     *  Returns null if no such parameter was in the request.
+     * 
+     **/
+
+    public String[] getStrings(HttpServletRequest request, String name);
+
+    /**
+     *  Returns the uploaded file with the specified parameter name,
+     *  or null if no such parameter was in the request.
+     * 
+     **/
+
+    public IUploadFile getUploadFile(HttpServletRequest request, String name);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/multipart/IPart.java b/tapestry-framework/src/org/apache/tapestry/multipart/IPart.java
new file mode 100644
index 0000000..12c0bc1
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/multipart/IPart.java
@@ -0,0 +1,37 @@
+//  Copyright 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.tapestry.multipart;
+
+/**
+ *  Common interface for data parts from multipart form submissions.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.0.1
+ * 
+ **/
+
+public interface IPart
+{
+    /**
+     *  Invoked at the end of a request cycle to delete any resources held by
+     *  the part.
+     * 
+     *  @see UploadPart#cleanup()
+     * 
+     **/
+
+    public void cleanup();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/multipart/UploadPart.java b/tapestry-framework/src/org/apache/tapestry/multipart/UploadPart.java
new file mode 100644
index 0000000..d7089e8
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/multipart/UploadPart.java
@@ -0,0 +1,139 @@
+//  Copyright 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.tapestry.multipart;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.commons.fileupload.FileItem;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.request.IUploadFile;
+
+/**
+ *  Portion of a multi-part request representing an uploaded file.
+ *
+ *  @author Joe Panico
+ *  @version $Id$
+ *  @since 2.0.1
+ *
+ **/
+public class UploadPart extends Object implements IUploadFile, IPart
+{
+    private static final Log LOG = LogFactory.getLog(UploadPart.class);
+
+    private FileItem _fileItem;
+
+    public UploadPart(FileItem fileItem)
+    {
+        if (fileItem == null)
+            throw new IllegalArgumentException(
+                Tapestry.format("invalid-null-parameter", "fileItem"));
+
+        _fileItem = fileItem;
+    }
+
+    public String getContentType()
+    {
+        return _fileItem.getContentType();
+    }
+
+    /**
+     *  Leverages {@link File} to convert the full file path and extract
+     *  the name.
+     * 
+     **/
+    public String getFileName()
+    {
+        File file = new File(this.getFilePath());
+
+        return file.getName();
+    }
+
+    /**
+     *  @since 2.0.4
+     * 
+     **/
+
+    public String getFilePath()
+    {
+        return _fileItem.getName();
+    }
+
+    public InputStream getStream()
+    {
+        try
+        {
+            return _fileItem.getInputStream();
+        }
+        catch (IOException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("UploadPart.unable-to-open-content-file", _fileItem.getName()),
+                ex);
+        }
+    }
+
+    /**
+     *  Deletes the external content file, if one exists.
+     * 
+     **/
+
+    public void cleanup()
+    {
+        _fileItem.delete();
+    }
+
+    /**
+     * Writes the uploaded content to a file.  This should be invoked at most once
+     * (perhaps we should add a check for this).  This will often
+     * be a simple file rename.
+     * 
+     * @since 3.0
+     */
+    public void write(File file)
+    {
+        try
+        {
+            _fileItem.write(file);
+        }
+        catch (Exception ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("UploadPart.write-failure", file, ex.getMessage()),
+                ex);
+        }
+    }
+
+    /**
+     * @since 3.0
+     */
+    public long getSize()
+    {
+        return _fileItem.getSize();
+    }
+
+    /**
+     * @since 3.0
+     */
+    public boolean isInMemory()
+    {
+        return _fileItem.isInMemory();
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/multipart/ValuePart.java b/tapestry-framework/src/org/apache/tapestry/multipart/ValuePart.java
new file mode 100644
index 0000000..efe27e1
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/multipart/ValuePart.java
@@ -0,0 +1,104 @@
+//  Copyright 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.tapestry.multipart;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *  A portion of a multipart request that stores a value, or values, for
+ *  a parameter.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.0.1
+ *
+ **/
+
+public class ValuePart implements IPart
+{
+    private int _count;
+    // Stores either String or List of String
+    private Object _value;
+
+    public ValuePart(String value)
+    {
+        _count = 1;
+        _value = value;
+    }
+
+    public int getCount()
+    {
+        return _count;
+    }
+
+    /**
+     *  Returns the value, or the first value (if multi-valued).
+     * 
+     **/
+
+    public String getValue()
+    {
+        if (_count == 1)
+            return (String) _value;
+
+        List l = (List) _value;
+
+        return (String) l.get(0);
+    }
+
+    /**
+     *  Returns the values as an array of strings.  If there is only one value,
+     *  it is returned wrapped as a single element array.
+     * 
+     **/
+
+    public String[] getValues()
+    {
+        if (_count == 1)
+            return new String[] {(String) _value };
+
+        List l = (List) _value;
+
+        return (String[]) l.toArray(new String[_count]);
+    }
+
+    public void add(String newValue)
+    {
+        if (_count == 1)
+        {
+            List l = new ArrayList();
+            l.add(_value);
+            l.add(newValue);
+
+            _value = l;
+            _count++;
+            return;
+        }
+
+        List l = (List) _value;
+        l.add(newValue);
+        _count++;
+    }
+
+    /**
+     *  Does nothing.
+     * 
+     **/
+
+    public void cleanup()
+    {
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/multipart/org.apache.tapestry.multipart.patch b/tapestry-framework/src/org/apache/tapestry/multipart/org.apache.tapestry.multipart.patch
new file mode 100644
index 0000000..47ceda0
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/multipart/org.apache.tapestry.multipart.patch
@@ -0,0 +1,1156 @@
+Index: DefaultMultipartDecoder.java

+===================================================================

+RCS file: /home/cvspublic/jakarta-tapestry/framework/src/org/apache/tapestry/multipart/DefaultMultipartDecoder.java,v

+retrieving revision 1.1

+diff -c -r1.1 DefaultMultipartDecoder.java

+*** DefaultMultipartDecoder.java	5 Mar 2003 22:59:49 -0000	1.1

+--- DefaultMultipartDecoder.java	21 Mar 2003 22:51:11 -0000

+***************

+*** 1,75 ****

+! /* ====================================================================

+!  * The Apache Software License, Version 1.1

+!  *

+!  * Copyright (c) 2000-2003 The Apache Software Foundation.  All rights

+!  * reserved.

+!  *

+!  * Redistribution and use in source and binary forms, with or without

+!  * modification, are permitted provided that the following conditions

+!  * are met:

+!  *

+!  * 1. Redistributions of source code must retain the above copyright

+!  *    notice, this list of conditions and the following disclaimer.

+!  *

+!  * 2. Redistributions in binary form must reproduce the above copyright

+!  *    notice, this list of conditions and the following disclaimer in

+!  *    the documentation and/or other materials provided with the

+!  *    distribution.

+!  *

+!  * 3. The end-user documentation included with the redistribution,

+!  *    if any, must include the following acknowledgment:

+!  *       "This product includes software developed by the

+!  *        Apache Software Foundation (http://apache.org/)."

+!  *    Alternately, this acknowledgment may appear in the software itself,

+!  *    if and wherever such third-party acknowledgments normally appear.

+!  *

+!  * 4. The names "Apache" and "Apache Software Foundation", "Tapestry" 

+!  *    must not be used to endorse or promote products derived from this

+!  *    software without prior written permission. For written

+!  *    permission, please contact apache@apache.org.

+!  *

+!  * 5. Products derived from this software may not be called "Apache" 

+!  *    or "Tapestry", nor may "Apache" or "Tapestry" appear in their 

+!  *    name, without prior written permission of the Apache Software Foundation.

+!  *

+!  * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED

+!  * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES

+!  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE

+!  * DISCLAIMED.  IN NO EVENT SHALL THE TAPESTRY CONTRIBUTOR COMMUNITY

+!  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,

+!  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT

+!  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF

+!  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND

+!  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,

+!  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT

+!  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF

+!  * SUCH DAMAGE.

+!  * ====================================================================

+!  *

+!  * This software consists of voluntary contributions made by many

+!  * individuals on behalf of the Apache Software Foundation.  For more

+!  * information on the Apache Software Foundation, please see

+!  * <http://www.apache.org/>.

+!  *

+   */

+- 

+  package org.apache.tapestry.multipart;

+  

+- import java.io.ByteArrayOutputStream;

+- import java.io.File;

+- import java.io.IOException;

+- import java.io.InputStream;

+- import java.io.OutputStream;

+  import java.util.HashMap;

+  import java.util.Iterator;

+  import java.util.Map;

+- 

+  import javax.servlet.http.HttpServletRequest;

+  

+  import org.apache.tapestry.ApplicationRuntimeException;

+- import org.apache.tapestry.Tapestry;

+  import org.apache.tapestry.request.IUploadFile;

+! import org.apache.tapestry.util.StringSplitter;

+  import org.apache.commons.logging.Log;

+  import org.apache.commons.logging.LogFactory;

+  

+--- 1,72 ----

+! /*

+!  *  ====================================================================

+!  *  The Apache Software License, Version 1.1

+!  *

+!  *  Copyright (c) 2002 The Apache Software Foundation.  All rights

+!  *  reserved.

+!  *

+!  *  Redistribution and use in source and binary forms, with or without

+!  *  modification, are permitted provided that the following conditions

+!  *  are met:

+!  *

+!  *  1. Redistributions of source code must retain the above copyright

+!  *  notice, this list of conditions and the following disclaimer.

+!  *

+!  *  2. Redistributions in binary form must reproduce the above copyright

+!  *  notice, this list of conditions and the following disclaimer in

+!  *  the documentation and/or other materials provided with the

+!  *  distribution.

+!  *

+!  *  3. The end-user documentation included with the redistribution,

+!  *  if any, must include the following acknowledgment:

+!  *  "This product includes software developed by the

+!  *  Apache Software Foundation (http://www.apache.org/)."

+!  *  Alternately, this acknowledgment may appear in the software itself,

+!  *  if and wherever such third-party acknowledgments normally appear.

+!  *

+!  *  4. The names "Apache" and "Apache Software Foundation" and

+!  *  "Apache Tapestry" must not be used to endorse or promote products

+!  *  derived from this software without prior written permission. For

+!  *  written permission, please contact apache@apache.org.

+!  *

+!  *  5. Products derived from this software may not be called "Apache",

+!  *  "Apache Tapestry", nor may "Apache" appear in their name, without

+!  *  prior written permission of the Apache Software Foundation.

+!  *

+!  *  THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED

+!  *  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES

+!  *  OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE

+!  *  DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR

+!  *  ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,

+!  *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT

+!  *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF

+!  *  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND

+!  *  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,

+!  *  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT

+!  *  OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF

+!  *  SUCH DAMAGE.

+!  *  ====================================================================

+!  *

+!  *  This software consists of voluntary contributions made by many

+!  *  individuals on behalf of the Apache Software Foundation.  For more

+!  *  information on the Apache Software Foundation, please see

+!  *  <http://www.apache.org/>.

+   */

+  package org.apache.tapestry.multipart;

+  

+  import java.util.HashMap;

+  import java.util.Iterator;

++ import java.util.List;

+  import java.util.Map;

+  import javax.servlet.http.HttpServletRequest;

+  

+  import org.apache.tapestry.ApplicationRuntimeException;

+  import org.apache.tapestry.request.IUploadFile;

+! 

+! import org.apache.commons.fileupload.FileItem;

+! import org.apache.commons.fileupload.FileUpload;

+! import org.apache.commons.fileupload.FileUploadException;

+! 

+  import org.apache.commons.logging.Log;

+  import org.apache.commons.logging.LogFactory;

+  

+***************

+*** 78,475 ****

+   *  file uploads and multi-valued parameters.  After decoding, the class is used

+   *  to access the parameter values.

+   * 

+!  *  <p>This implementation is partially based on the MultipartRequest from

+!  *  <a href="http://sf.net/projects/jetty">Jetty</a> (which is LGPL), and

+!  *  partly from research on the web, including a discussion of the 

+!  *  <a href="http://www.cis.ohio-state.edu/cgi-bin/rfc/rfc1867.html">RFC</a>.

+   *  

+   *  <p>Supports single valued parameters, multi-valued parameters and individual

+   *  file uploads.  That is, for file uploads, each upload must be a unique parameter

+!  *  (that is all the {@link org.apache.tapestry.form.Upload} component needs).

+  

+   *

+!  *  @author Howard Lewis Ship

+!  *  @version $Id: DefaultMultipartDecoder.java,v 1.1 2003/03/05 22:59:49 hlship Exp $

+   *  @since 2.0.1

+   *

+   **/

+! 

+! public class DefaultMultipartDecoder implements IMultipartDecoder

+  {

+!     private static final Log LOG = LogFactory.getLog(DefaultMultipartDecoder.class);

+! 

+!     public static final String MULTIPART_FORM_DATA_CONTENT_TYPE = "multipart/form-data";

+! 

+!     private static final String QUOTE = "\"";

+! 

+!     private Map partMap = new HashMap();

+! 

+!     public static boolean isMultipartRequest(HttpServletRequest request)

+!     {

+!         String contentType = request.getContentType();

+! 

+!         if (contentType == null)

+!             return false;

+! 

+!         return contentType.startsWith(MULTIPART_FORM_DATA_CONTENT_TYPE);

+!     }

+! 

+!     private void close(InputStream stream)

+!     {

+!         try

+!         {

+!             if (stream != null)

+!                 stream.close();

+!         }

+!         catch (IOException ex)

+!         {

+!             // Ignore.

+!         }

+!     }

+! 

+!     private static final String BOUNDARY = "boundary=";

+! 

+!     public void decode(HttpServletRequest request)

+!     {

+!         if (!isMultipartRequest(request))

+!             throw new ApplicationRuntimeException(

+!                 Tapestry.getString(

+!                     "MultipartDecoder.wrong-content-type",

+!                     request.getContentType()));

+! 

+!         String contentType = request.getContentType();

+!         int pos = contentType.indexOf(BOUNDARY);

+! 

+!         String boundaryString = "--" + contentType.substring(pos + BOUNDARY.length());

+!         byte[] boundary = (boundaryString + "--").getBytes();

+! 

+!         LineInput input = null;

+! 

+!         try

+!         {

+!             input = new LineInput(request.getInputStream());

+! 

+!             checkForInitialBoundary(input, boundaryString);

+! 

+!             boolean last = false;

+! 

+!             while (!last)

+!             {

+!                 last = readNextPart(input, boundary);

+!             }

+!         }

+!         catch (IOException ex)

+!         {

+!             LOG.error(

+!                 Tapestry.getString("MultipartDecoder.io-exception-reading-input", ex.getMessage()),

+!                 ex);

+! 

+!             // Cleanup any partial upload files.

+! 

+!             cleanup();

+! 

+!             throw new ApplicationRuntimeException(ex);

+!         }

+!         finally

+!         {

+!             //		close(input);

+!         }

+! 

+!     }

+! 

+!     private void checkForInitialBoundary(LineInput input, String boundary) throws IOException

+!     {

+!         String line = input.readLine();

+! 

+!         if (line != null && line.equals(boundary))

+!             return;

+! 

+!         throw new ApplicationRuntimeException(

+!             Tapestry.getString("MultipartDecoder.missing-initial-boundary"));

+!     }

+! 

+!     private boolean readNextPart(LineInput input, byte[] boundary) throws IOException

+!     {

+!         String disposition = null;

+!         String contentType = null;

+! 

+!         // First read the various headers (before the content)

+! 

+!         while (true)

+!         {

+!             String line = input.readLine();

+! 

+!             if (line == null || line.length() == 0)

+!                 break;

+! 

+!             int colonx = line.indexOf(':');

+! 

+!             if (colonx > 0)

+!             {

+!                 String key = line.substring(0, colonx).toLowerCase();

+! 

+!                 if (key.equals("content-disposition"))

+!                 {

+!                     disposition = line.substring(colonx + 1).trim();

+!                     continue;

+!                 }

+! 

+!                 if (key.equals("content-type"))

+!                 {

+!                     contentType = line.substring(colonx + 1).trim();

+!                     continue;

+!                 }

+!             }

+! 

+!         }

+! 

+!         if (disposition == null)

+!             throw new ApplicationRuntimeException(

+!                 Tapestry.getString("MultipartDecoder.missing-content-disposition"));

+! 

+!         Map dispositionMap = explodeDisposition(disposition);

+!         String name = (String) dispositionMap.get("name");

+! 

+!         if (Tapestry.isNull(name))

+!             throw new ApplicationRuntimeException(

+!                 Tapestry.getString("MultipartDecoder.invalid-content-disposition", disposition));

+! 

+!         if (!dispositionMap.containsKey("filename"))

+!             return readValuePart(input, boundary, name);

+! 

+!         String fileName = (String) dispositionMap.get("filename");

+! 

+!         return readFilePart(input, boundary, name, fileName, contentType);

+!     }

+! 

+!     private static StringSplitter splitter = new StringSplitter(';');

+! 

+!     private Map explodeDisposition(String disposition)

+!     {

+!         Map result = new HashMap();

+! 

+!         String[] elements = splitter.splitToArray(disposition);

+! 

+!         for (int i = 0; i < elements.length; i++)

+!         {

+!             String element = elements[i];

+!             int x = element.indexOf('=');

+! 

+!             if (x < 0)

+!                 continue;

+! 

+!             String key = element.substring(0, x).trim();

+!             String rawValue = element.substring(x + 1);

+! 

+!             if (!(rawValue.startsWith(QUOTE) && rawValue.endsWith(QUOTE)))

+!                 throw new ApplicationRuntimeException(

+!                     Tapestry.getString(

+!                         "MultipartDecoder.invalid-content-disposition",

+!                         disposition));

+! 

+!             result.put(key, rawValue.substring(1, rawValue.length() - 1));

+! 

+!         }

+! 

+!         return result;

+!     }

+! 

+!     private boolean readFilePart(

+!         LineInput input,

+!         byte[] boundary,

+!         String name,

+!         String fileName,

+!         String contentType)

+!         throws IOException

+!     {

+!         UploadOutputStream uploadStream = new UploadOutputStream();

+! 

+!         boolean last = readIntoStream(input, boundary, uploadStream);

+! 

+!         uploadStream.close();

+! 

+!         File file = uploadStream.getContentFile();

+! 

+!         UploadPart p;

+! 

+!         if (LOG.isDebugEnabled())

+!             LOG.debug("Read file part '" + name + "'.");

+! 

+!         if (file != null)

+!             p = new UploadPart(fileName, contentType, file);

+!         else

+!             p = new UploadPart(fileName, contentType, uploadStream.getContent());

+! 

+!         partMap.put(name, p);

+! 

+!         return last;

+!     }

+! 

+!     private boolean readValuePart(LineInput input, byte[] boundary, String name) throws IOException

+!     {

+!         ByteArrayOutputStream baos = new ByteArrayOutputStream();

+! 

+!         boolean last = readIntoStream(input, boundary, baos);

+! 

+!         baos.close();

+! 

+!         String value = baos.toString();

+! 

+!         if (LOG.isDebugEnabled())

+!             LOG.debug("Read value part '" + name + "' with value: " + value);

+! 

+!         ValuePart p = (ValuePart) partMap.get(name);

+! 

+!         if (p == null)

+!         {

+!             p = new ValuePart(value);

+!             partMap.put(name, p);

+!         }

+!         else

+!             p.add(value);

+! 

+!         return last;

+!     }

+! 

+!     private static final int CR = 13;

+!     private static final int LF = 10;

+!     private static final int SPECIAL = -2;

+! 

+!     /**

+!      *  Copies the input stream into the output stream, stopping once the boundary is seen

+!      *  (the boundary is not copied).  Returns true when the input stream is exhausted,

+!      *  false otherwise.

+!      * 

+!      *  This is an ugly cut and past of ugly code from Jetty.  This really needs to be fixed!

+!      * 

+!      **/

+! 

+!     private boolean readIntoStream(LineInput input, byte[] boundary, OutputStream stream)

+!         throws IOException

+!     {

+!         boolean result = false;

+!         int c = 0;

+!         boolean cr = false;

+!         boolean lf = false;

+!         int _char = SPECIAL;

+!         boolean more = true;

+! 

+!         while (true)

+!         {

+!             int b = 0;

+! 

+!             while (more)

+!             {

+!                 c = (_char != SPECIAL) ? _char : input.read();

+! 

+!                 if (c == -1)

+!                 {

+!                     more = false;

+!                     continue;

+!                 }

+! 

+!                 _char = SPECIAL;

+! 

+!                 // look for CR and/or LF

+!                 if (c == CR || c == LF)

+!                 {

+!                     if (c == CR)

+!                         _char = input.read();

+!                     break;

+!                 }

+! 

+!                 // look for boundary

+!                 if (b >= 0 && b < boundary.length && c == boundary[b])

+!                     b++;

+!                 else

+!                 {

+!                     // this is not a boundary

+!                     if (cr)

+!                         stream.write(CR);

+!                     if (lf)

+!                         stream.write(LF);

+!                     cr = lf = false;

+! 

+!                     if (b > 0)

+!                         stream.write(boundary, 0, b);

+!                     b = -1;

+! 

+!                     stream.write(c);

+!                 }

+!             }

+! 

+!             // check partial boundary

+!             if ((b > 0 && b < boundary.length - 2) || (b == boundary.length - 1))

+!             {

+!                 stream.write(boundary, 0, b);

+!                 b = -1;

+!             }

+! 

+!             // boundary match

+!             if (b > 0 || c == -1)

+!             {

+!                 if (b == boundary.length)

+!                     result = true;

+! 

+!                 if (_char == LF)

+!                     _char = SPECIAL;

+! 

+!                 break;

+!             }

+! 

+!             // handle CR LF

+!             if (cr)

+!                 stream.write(CR);

+!             if (lf)

+!                 stream.write(LF);

+! 

+!             cr = (c == CR);

+!             lf = (c == LF || _char == LF);

+! 

+!             if (_char == LF)

+!                 _char = SPECIAL;

+!         }

+! 

+!         return result;

+!     }

+  

+      /**

+       *  Invokes {@link IPart#cleanup()} on each part.

+       * 

+       **/

+- 

+      public void cleanup()

+      {

+!         Iterator i = partMap.values().iterator();

+          while (i.hasNext())

+          {

+              IPart part = (IPart) i.next();

+              part.cleanup();

+          }

+      }

+  

+!     public String getString(String name)

+!     {

+!         ValuePart p = (ValuePart) partMap.get(name);

+! 

+!         if (p == null)

+!             return null;

+! 

+!         return p.getValue();

+!     }

+! 

+!     public String[] getStrings(String name)

+!     {

+!         ValuePart p = (ValuePart) partMap.get(name);

+! 

+!         if (p == null)

+!             return null;

+! 

+!         return p.getValues();

+!     }

+! 

+!     public IUploadFile getUploadFile(String name)

+!     {

+!         return (UploadPart) partMap.get(name);

+!     }

+! }

+\ No newline at end of file

+--- 75,266 ----

+   *  file uploads and multi-valued parameters.  After decoding, the class is used

+   *  to access the parameter values.

+   * 

+!  *  <p>This implementation is a thin wrapper around the Apache Jakarta

+!  *  <a href="http://jakarta.apache.org/commons/fileupload/">FileUpload</a>. 

+   *  

+   *  <p>Supports single valued parameters, multi-valued parameters and individual

+   *  file uploads.  That is, for file uploads, each upload must be a unique parameter

+!  *  (that is all the {@link net.sf.tapestry.form.Upload} component needs).

+  

+   *

+!  *  @author Joe Panico

+!  *  @version $Id: DefaultMultipartDecoder.java,v 1.1 2003/01/16 00:53:56 hlship Exp $

+   *  @since 2.0.1

+   *

+   **/

+! public class DefaultMultipartDecoder

+! 	extends Object

+! 	implements IMultipartDecoder

+  {

+! 	public static final int DEFAULT_MAX_SIZE = 10000000;

+! 	public static final int DEFAULT_THRESHOLD_SIZE = 1024;

+! 	public static final String DEFAULT_REPOSITORY_PATH = System.getProperty("java.io.tmpdir");

+! 	 	

+! 	private static final Log LOG =

+! 		LogFactory.getLog(DefaultMultipartDecoder.class);

+! 		

+! 	private static int _maxSize = DEFAULT_MAX_SIZE;

+! 	private static int _thresholdSize = DEFAULT_THRESHOLD_SIZE;

+! 	private static String _repositoryPath = DEFAULT_REPOSITORY_PATH;

+! 	

+! 	private Map _partMap = new HashMap();

+! 

+! 

+! 	public static void setMaxSize(int maxSize)

+! 	{

+! 		_maxSize = maxSize;

+! 	}

+! 	

+! 	public static int getMaxSize()

+! 	{

+! 		return _maxSize;

+! 	}

+! 	

+! 	public static  void setThresholdSize(int thresholdSize)

+! 	{

+! 		_thresholdSize = thresholdSize;

+! 	}

+! 	

+! 	public static int getThresholdSize()

+! 	{

+! 		return _thresholdSize;

+! 	}

+! 	

+! 	public static void setRepositoryPath(String repositoryPath)

+! 	{

+! 		_repositoryPath = repositoryPath;

+! 	}

+! 	

+! 	public static String getRepositoryPath()

+! 	{

+! 		return _repositoryPath;

+! 	}

+! 	

+! 	public static boolean isMultipartRequest(HttpServletRequest request)

+! 	{

+! 		return FileUpload.isMultipartContent(request);

+! 	}

+  

+      /**

+       *  Invokes {@link IPart#cleanup()} on each part.

+       * 

+       **/

+      public void cleanup()

+      {

+!         Iterator i = _partMap.values().iterator();

+          while (i.hasNext())

+          {

+              IPart part = (IPart) i.next();

+              part.cleanup();

+          }

+      }

++ 	

++ 	/**

++ 	 * @see net.sf.tapestry.multipart.IMultipartDecoder#decode(HttpServletRequest)

++ 	 * 

++ 	 * @throws ApplicationRuntimeException if decode fails, for instance the

++ 	 * request exceeds getMaxSize()

++ 	 */

++ 	public void decode(HttpServletRequest request)

++ 	{

++ 		if (request != null)

++ 		{

++ 			try

++ 			{

++ 				FileUpload aFileUpload = new FileUpload();

++ 

++ 				aFileUpload.setSizeMax(_maxSize);

++ 				aFileUpload.setSizeThreshold(_thresholdSize);

++ 				aFileUpload.setRepositoryPath(_repositoryPath);

++ 

++ 				List someParts = aFileUpload.parseRequest(request);

++ 

++ 				if ((someParts != null) && (someParts.size() > 0))

++ 				{

++ 					Iterator aPartIterator = someParts.iterator();

++ 					while (aPartIterator.hasNext())

++ 					{

++ 						FileItem aPart = (FileItem) aPartIterator.next();

++ 						if (aPart.isFormField())

++ 						{

++ 							String aPartName = aPart.getFieldName();

++ 							ValuePart aValuePart = (ValuePart) _partMap.get(aPartName);

++ 							if (aValuePart != null)

++ 							{

++ 								aValuePart.add(aPart.getString());

++ 							}

++ 							else

++ 							{

++ 								aValuePart = new ValuePart(aPart.getString());

++ 								_partMap.put(aPartName, aValuePart);

++ 							}

++ 						}

++ 						else

++ 						{

++ 							UploadPart aFile = new UploadPart(aPart);

++ 

++ 							_partMap.put(aPart.getFieldName(), aFile);

++ 						}

++ 					}

++ 				}

++ 			}

++ 			catch (FileUploadException e)

++ 			{

++ 				LOG.warn("decode", e);

++ 				throw new ApplicationRuntimeException(e);

++ 			}

++ 		}

++ 

++ 	}

++ 

++ 	/**

++ 	 * @see net.sf.tapestry.multipart.IMultipartDecoder#getString(String)

++ 	 */

++ 	public String getString(String name)

++ 	{

++ 		String getString = null;

++ 

++ 		if (name != null)

++ 		{

++ 			ValuePart aPart = (ValuePart) _partMap.get(name);

++ 			if (aPart != null)

++ 			{

++ 				getString = aPart.getValue();

++ 			}

++ 		}

++ 		return getString;

++ 	}

++ 

++ 	/**

++ 	 * @see net.sf.tapestry.multipart.IMultipartDecoder#getStrings(String)

++ 	 */

++ 	public String[] getStrings(String name)

++ 	{

++ 		String[] getStrings = null;

++ 

++ 		if (name != null)

++ 		{

++ 			ValuePart aPart = (ValuePart) _partMap.get(name);

++ 			if (aPart != null)

++ 			{

++ 				getStrings = aPart.getValues();

++ 			}

++ 		}

++ 		return getStrings;

++ 	}

++ 

++ 	/**

++ 	 * @see net.sf.tapestry.multipart.IMultipartDecoder#getUploadFile(String)

++ 	 */

++ 	public IUploadFile getUploadFile(String name)

++ 	{

++ 		IUploadFile getUploadFile = null;

++ 

++ 		if (name != null)

++ 		{

++ 			getUploadFile = (IUploadFile) _partMap.get(name);

++ 		}

++ 		return getUploadFile;

++ 	}

+  

+! }

+Index: UploadPart.java

+===================================================================

+RCS file: /home/cvspublic/jakarta-tapestry/framework/src/org/apache/tapestry/multipart/UploadPart.java,v

+retrieving revision 1.1

+diff -c -r1.1 UploadPart.java

+*** UploadPart.java	5 Mar 2003 22:59:49 -0000	1.1

+--- UploadPart.java	21 Mar 2003 22:51:11 -0000

+***************

+*** 1,193 ****

+! /* ====================================================================

+!  * The Apache Software License, Version 1.1

+!  *

+!  * Copyright (c) 2000-2003 The Apache Software Foundation.  All rights

+!  * reserved.

+!  *

+!  * Redistribution and use in source and binary forms, with or without

+!  * modification, are permitted provided that the following conditions

+!  * are met:

+!  *

+!  * 1. Redistributions of source code must retain the above copyright

+!  *    notice, this list of conditions and the following disclaimer.

+!  *

+!  * 2. Redistributions in binary form must reproduce the above copyright

+!  *    notice, this list of conditions and the following disclaimer in

+!  *    the documentation and/or other materials provided with the

+!  *    distribution.

+!  *

+!  * 3. The end-user documentation included with the redistribution,

+!  *    if any, must include the following acknowledgment:

+!  *       "This product includes software developed by the

+!  *        Apache Software Foundation (http://apache.org/)."

+!  *    Alternately, this acknowledgment may appear in the software itself,

+!  *    if and wherever such third-party acknowledgments normally appear.

+!  *

+!  * 4. The names "Apache" and "Apache Software Foundation", "Tapestry" 

+!  *    must not be used to endorse or promote products derived from this

+!  *    software without prior written permission. For written

+!  *    permission, please contact apache@apache.org.

+!  *

+!  * 5. Products derived from this software may not be called "Apache" 

+!  *    or "Tapestry", nor may "Apache" or "Tapestry" appear in their 

+!  *    name, without prior written permission of the Apache Software Foundation.

+!  *

+!  * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED

+!  * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES

+!  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE

+!  * DISCLAIMED.  IN NO EVENT SHALL THE TAPESTRY CONTRIBUTOR COMMUNITY

+!  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,

+!  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT

+!  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF

+!  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND

+!  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,

+!  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT

+!  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF

+!  * SUCH DAMAGE.

+!  * ====================================================================

+!  *

+!  * This software consists of voluntary contributions made by many

+!  * individuals on behalf of the Apache Software Foundation.  For more

+!  * information on the Apache Software Foundation, please see

+!  * <http://www.apache.org/>.

+!  *

+   */

+- 

+  package org.apache.tapestry.multipart;

+  

+- import java.io.ByteArrayInputStream;

+  import java.io.File;

+- import java.io.FileInputStream;

+  import java.io.IOException;

+  import java.io.InputStream;

+  

+  import org.apache.commons.logging.Log;

+  import org.apache.commons.logging.LogFactory;

+  

+- import org.apache.tapestry.ApplicationRuntimeException;

+- import org.apache.tapestry.Tapestry;

+- import org.apache.tapestry.request.IUploadFile;

+  

+  /**

+   *  Portion of a multi-part request representing an uploaded file.

+   *

+!  *  @author Howard Lewis Ship

+!  *  @version $Id: UploadPart.java,v 1.1 2003/03/05 22:59:49 hlship Exp $

+   *  @since 2.0.1

+   *

+   **/

+! 

+! public class UploadPart implements IUploadFile, IPart

+  {

+!     private static final Log LOG = LogFactory.getLog(UploadPart.class);

+  

+!     private byte[] _content;

+!     private File _contentFile;

+!     private String _filePath;

+!     private String _contentType;

+! 

+!     public UploadPart(String filePath, String contentType, byte[] content)

+!     {

+!         _filePath = filePath;

+!         _contentType = contentType;

+!         _content = content;

+!     }

+! 

+!     public UploadPart(String filePath, String contentType, File contentFile)

+!     {

+!         _filePath = filePath;

+!         _contentType = contentType;        

+!         _contentFile = contentFile;

+!     }

+  

+  	/**

+  	 *  @since 2.0.4

+  	 * 

+  	 **/

+! 	

+!     public String getFilePath()

+!     {

+!         return _filePath;

+!     }

+  

+  	/**

+! 	 *  Always returns false, at least so far.  Future enhancements

+! 	 *  may involve truncating the input if it exceeds a certain

+! 	 *  size or upload time (such things may be used for denial

+! 	 *  of service attacks).

+! 	 * 

+! 	 **/

+! 	

+!     public boolean isTruncated()

+!     {

+!         return false;

+!     }

+! 

+!     public InputStream getStream()

+!     {

+!         if (_content != null)

+!             return new ByteArrayInputStream(_content);

+! 

+!         try

+!         {

+!             return new FileInputStream(_contentFile);

+!         }

+!         catch (IOException ex)

+!         {

+!             throw new ApplicationRuntimeException(

+!                 Tapestry.getString(

+!                     "UploadPart.unable-to-open-content-file",

+!                     _filePath,

+!                     _contentFile.getAbsolutePath()),

+!                 ex);

+!         }

+!     }

+  

+!     /**

+!      *  Deletes the external content file, if one exists.

+!      * 

+!      **/

+  

+-     public void cleanup()

+-     {

+-         if (_contentFile != null)

+-         {

+-             if (LOG.isDebugEnabled())

+-                 LOG.debug("Deleting upload file " + _contentFile + ".");

+- 

+-             boolean success = _contentFile.delete();

+- 

+-             if (!success)

+-                 LOG.warn(

+-                     Tapestry.getString(

+-                         "UploadPart.temporary-file-not-deleted",

+-                         _contentFile.getAbsolutePath()));

+- 

+-             // In rare cases (when exceptions are thrown while the request

+-             // is decoded), cleanup() may be called multiple times.

+- 

+-             _contentFile = null;

+-         }

+- 

+-     }

+-     

+-     

+      /**

+!      *  Leverages {@link File} to convert the full file path and extract

+!      *  the name.

+       * 

+       **/

+!     

+!     public String getFileName()

+!     {

+!   		File file = new File(_filePath);

+!   		

+!   		return file.getName();

+!     }

+! 

+!     public String getContentType()

+!     {

+!         return _contentType;

+!     }

+  

+! }

+\ No newline at end of file

+--- 1,201 ----

+! /*

+!  *  ====================================================================

+!  *  The Apache Software License, Version 1.1

+!  *

+!  *  Copyright (c) 2002 The Apache Software Foundation.  All rights

+!  *  reserved.

+!  *

+!  *  Redistribution and use in source and binary forms, with or without

+!  *  modification, are permitted provided that the following conditions

+!  *  are met:

+!  *

+!  *  1. Redistributions of source code must retain the above copyright

+!  *  notice, this list of conditions and the following disclaimer.

+!  *

+!  *  2. Redistributions in binary form must reproduce the above copyright

+!  *  notice, this list of conditions and the following disclaimer in

+!  *  the documentation and/or other materials provided with the

+!  *  distribution.

+!  *

+!  *  3. The end-user documentation included with the redistribution,

+!  *  if any, must include the following acknowledgment:

+!  *  "This product includes software developed by the

+!  *  Apache Software Foundation (http://www.apache.org/)."

+!  *  Alternately, this acknowledgment may appear in the software itself,

+!  *  if and wherever such third-party acknowledgments normally appear.

+!  *

+!  *  4. The names "Apache" and "Apache Software Foundation" and

+!  *  "Apache Tapestry" must not be used to endorse or promote products

+!  *  derived from this software without prior written permission. For

+!  *  written permission, please contact apache@apache.org.

+!  *

+!  *  5. Products derived from this software may not be called "Apache",

+!  *  "Apache Tapestry", nor may "Apache" appear in their name, without

+!  *  prior written permission of the Apache Software Foundation.

+!  *

+!  *  THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED

+!  *  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES

+!  *  OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE

+!  *  DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR

+!  *  ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,

+!  *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT

+!  *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF

+!  *  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND

+!  *  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,

+!  *  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT

+!  *  OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF

+!  *  SUCH DAMAGE.

+!  *  ====================================================================

+!  *

+!  *  This software consists of voluntary contributions made by many

+!  *  individuals on behalf of the Apache Software Foundation.  For more

+!  *  information on the Apache Software Foundation, please see

+!  *  <http://www.apache.org/>.

+   */

+  package org.apache.tapestry.multipart;

+  

+  import java.io.File;

+  import java.io.IOException;

+  import java.io.InputStream;

+  

++ import org.apache.tapestry.ApplicationRuntimeException;

++ import org.apache.tapestry.request.IUploadFile;

++ 

++ import org.apache.commons.fileupload.DefaultFileItem;

++ import org.apache.commons.fileupload.FileItem;

+  import org.apache.commons.logging.Log;

+  import org.apache.commons.logging.LogFactory;

+  

+  

+  /**

+   *  Portion of a multi-part request representing an uploaded file.

+   *

+!  *  @author Joe Panico

+!  *  @version $Id: UploadPart.java,v 1.10 2003/01/13 03:33:23 hlship Exp $

+   *  @since 2.0.1

+   *

+   **/

+! public class UploadPart extends Object 

+! 	implements IUploadFile, IPart

+  {

+! 	private static final Log LOG =

+! 		LogFactory.getLog(UploadPart.class);

+! 		

+! 	FileItem _fileItem;

+! 	

+! 	public UploadPart(FileItem fileItem)

+! 	{

+! 		if( fileItem != null)

+! 		{

+! 			_fileItem = fileItem;

+! 		}

+! 		else

+! 		{

+! 			String aMessage = "UploadPart(FileItem) does not accept null args";

+! 			throw new IllegalArgumentException( aMessage);

+! 		}

+! 	}

+! 	

+! 	public static UploadPart newInstance(

+! 		String path,

+! 		String name,

+! 		String contentType,

+! 		int requestSize,

+! 		int threshold)

+! 	{

+! 		UploadPart newInstance = null;

+! 		FileItem aFileItem =

+! 			DefaultFileItem.newInstance(

+! 				path,

+! 				name,

+! 				contentType,

+! 				requestSize,

+! 				threshold);

+! 		

+! 		if( aFileItem != null)

+! 		{

+! 			newInstance = new UploadPart( aFileItem);

+! 		}

+! 		return newInstance;

+! 	}

+!                                        

+! 	/**

+! 	 * @see net.sf.tapestry.IUploadFile#getContentType()

+! 	 */

+! 	public String getContentType()

+! 	{

+! 		return _fileItem.getContentType();

+! 	}

+  

+!     /**

+!      *  Leverages {@link File} to convert the full file path and extract

+!      *  the name.

+!      * 

+!      **/

+! 	public String getFileName()

+! 	{

+!   		File aFile = new File( this.getFilePath() );

+!   		

+!   		return aFile.getName();

+! 	}

+  

+  	/**

+  	 *  @since 2.0.4

+  	 * 

+  	 **/

+! 	public String getFilePath()

+! 	{

+! 		return _fileItem.getName();

+! 	}

+  

+  	/**

+! 	 * @see net.sf.tapestry.IUploadFile#getStream()

+! 	 */

+! 	public InputStream getStream()

+! 	{

+! 		InputStream getStream = null;

+! 		

+! 		try

+! 		{

+! 			getStream = _fileItem.getInputStream();

+! 		}

+! 		catch (IOException e)

+! 		{

+! 			LOG.warn("getStream", e);

+! 			

+! 			throw new ApplicationRuntimeException(

+! 				"Unable to open uploaded file",

+! 				e);

+! 		}

+! 		

+! 		return getStream;

+! 	}

+  

+! 	/**

+! 	 * The current implementation does not truncate. Parts that are too

+! 	 * large cause a decode failure

+! 	 */

+! 	public boolean isTruncated()

+! 	{

+! 		return false;

+! 	}

+  

+      /**

+!      *  Deletes the external content file, if one exists.

+       * 

+       **/

+! 	public void cleanup()

+! 	{

+! 		_fileItem.delete();

+! 		// unfortunately FileItem.delete() does not signal success, so

+! 		// we rely on a little more internal knowledge than is ideal

+! 		File aStorageLocation = _fileItem.getStoreLocation();

+! 		if( (aStorageLocation != null) && (aStorageLocation.exists()) )

+! 		{

+! 			String aMessage = "Unable to delete FileItem: " + _fileItem;

+! 			

+! 			throw new ApplicationRuntimeException(aMessage);

+! 		}

+! 	}

+  

+! }

diff --git a/tapestry-framework/src/org/apache/tapestry/package.html b/tapestry-framework/src/org/apache/tapestry/package.html
new file mode 100644
index 0000000..2821b51
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/package.html
@@ -0,0 +1,45 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+<b>Tapestry</b> is a comprehensive web application framework, written in Java.
+
+<p>Tapestry is not an application server.
+It is designed to be used inside an application server.
+
+<p>Tapestry is not an application.
+Tapestry is a framework for creating web applications.
+
+<p>Tapestry is not a way of using JavaServer Pages.
+Tapestry is an <i>alternative to</i> using JavaServer Pages.
+
+<p>Tapestry is not a scripting environment.
+Tapestry uses a component object model, not simple scripting,
+to create highly dynamic, interactive web pages.
+
+<p>Tapestry is based on the Java Servlet API version 2.2.
+
+<p>Tapestry uses a sophisticated component model to divide a web application into
+a hierarchy of {@link org.apache.tapestry.IComponent components}.
+Each component has specific responsibilities for rendering web pages
+(that is, generating a portion of an HTML page) and responding to HTML queries
+(such as clicking on a link, or submitting a form).
+
+<p>The Tapestry framework takes on virtually all of the responsibilities
+for managing application flow and server-side client state.  
+This allows developers to concentrate on the business and presentation aspects of the application.
+
+<hr>
+
+<p>Visit Tapestry's home page at
+<a href="http://jakarta.apache.org/tapestry">http://jakarta.apache.org/tapestry</a>
+for more details on licensing.
+
+@author Howard Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/pageload/ComponentTreeWalker.java b/tapestry-framework/src/org/apache/tapestry/pageload/ComponentTreeWalker.java
new file mode 100644
index 0000000..f309497
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/pageload/ComponentTreeWalker.java
@@ -0,0 +1,61 @@
+//  Copyright 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.tapestry.pageload;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Walks through the tree of components and invokes the visitors on each of 
+ *  of the components in the tree.
+ * 
+ *  @author mindbridge
+ *  @version $Id$
+ *  @since 3.0
+ */
+public class ComponentTreeWalker
+{
+    private IComponentVisitor[] _visitors;
+    
+    public ComponentTreeWalker(IComponentVisitor[] visitors)
+    {
+        _visitors = visitors;
+    }
+
+    public void walkComponentTree(IComponent component)
+    {
+        // Invoke visitors
+        for (int i = 0; i < _visitors.length; i++)
+        {
+            IComponentVisitor visitor = _visitors[i];
+            visitor.visitComponent(component);
+        }
+
+        // Recurse into the embedded components
+        Collection components = component.getComponents().values();
+
+        if (Tapestry.size(components) == 0)
+            return;
+
+        for (Iterator it = components.iterator(); it.hasNext();)
+        {
+            IComponent embedded = (IComponent) it.next();
+            walkComponentTree(embedded);
+        }
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/pageload/EstablishDefaultParameterValuesVisitor.java b/tapestry-framework/src/org/apache/tapestry/pageload/EstablishDefaultParameterValuesVisitor.java
new file mode 100644
index 0000000..11a355b
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/pageload/EstablishDefaultParameterValuesVisitor.java
@@ -0,0 +1,83 @@
+//  Copyright 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.tapestry.pageload;
+
+import java.util.Iterator;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.binding.ExpressionBinding;
+import org.apache.tapestry.spec.IComponentSpecification;
+import org.apache.tapestry.spec.IParameterSpecification;
+
+/**
+ *  For all parameters in the examined component that have default values, but are not bound,
+ *  automatically add an ExpressionBinding with the default value.
+ *   
+ *  @author mindbridge
+ *  @version $Id$
+ *  @since 3.0
+ */
+public class EstablishDefaultParameterValuesVisitor implements IComponentVisitor
+{
+    private IResourceResolver _resolver;
+    
+    public EstablishDefaultParameterValuesVisitor(IResourceResolver resolver)
+    {
+        _resolver = resolver;
+    }
+    
+    /**
+     * @see org.apache.tapestry.pageload.IComponentVisitor#visitComponent(org.apache.tapestry.IComponent)
+     */
+    public void visitComponent(IComponent component)
+    {
+        IComponentSpecification spec = component.getSpecification();
+
+        Iterator i = spec.getParameterNames().iterator();
+
+        while (i.hasNext())
+        {
+            String name = (String) i.next();
+            IParameterSpecification parameterSpec = spec.getParameter(name);
+
+            String defaultValue = parameterSpec.getDefaultValue();
+            if (defaultValue == null)
+                continue;
+            
+            // the parameter has a default value, so it must not be required
+            if (parameterSpec.isRequired())
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "EstablishDefaultParameterValuesVisitor.parameter-must-have-no-default-value",
+                    component.getExtendedId(),
+                    name),
+                component,
+                parameterSpec.getLocation(),
+                null);
+            
+            // if there is no binding for this parameter, bind it to the default value
+            if (component.getBinding(name) == null) {
+                IBinding binding = new ExpressionBinding(_resolver, component, defaultValue, parameterSpec.getLocation());
+                component.setBinding(name, binding);
+            }
+                
+        }
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/pageload/IComponentVisitor.java b/tapestry-framework/src/org/apache/tapestry/pageload/IComponentVisitor.java
new file mode 100644
index 0000000..6ca0365
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/pageload/IComponentVisitor.java
@@ -0,0 +1,29 @@
+//  Copyright 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.tapestry.pageload;
+
+import org.apache.tapestry.IComponent;
+
+/**
+ *  An interface defining an entity that is interested in examining a particular component 
+ * 
+ *  @author mindbridge
+ *  @version $Id$
+ *  @since 3.0
+ */
+public interface IComponentVisitor
+{
+    void visitComponent(IComponent component);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/pageload/PageLoader.java b/tapestry-framework/src/org/apache/tapestry/pageload/PageLoader.java
new file mode 100644
index 0000000..d48d4fa
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/pageload/PageLoader.java
@@ -0,0 +1,964 @@
+//  Copyright 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.tapestry.pageload;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.BaseComponent;
+import org.apache.tapestry.IAsset;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.INamespace;
+import org.apache.tapestry.IPage;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.asset.ContextAsset;
+import org.apache.tapestry.asset.ExternalAsset;
+import org.apache.tapestry.asset.PrivateAsset;
+import org.apache.tapestry.binding.ExpressionBinding;
+import org.apache.tapestry.binding.FieldBinding;
+import org.apache.tapestry.binding.ListenerBinding;
+import org.apache.tapestry.binding.StaticBinding;
+import org.apache.tapestry.binding.StringBinding;
+import org.apache.tapestry.engine.IComponentClassEnhancer;
+import org.apache.tapestry.engine.IPageLoader;
+import org.apache.tapestry.engine.ISpecificationSource;
+import org.apache.tapestry.engine.ITemplateSource;
+import org.apache.tapestry.event.PageDetachListener;
+import org.apache.tapestry.html.BasePage;
+import org.apache.tapestry.request.RequestContext;
+import org.apache.tapestry.resolver.ComponentSpecificationResolver;
+import org.apache.tapestry.resource.ClasspathResourceLocation;
+import org.apache.tapestry.resource.ContextResourceLocation;
+import org.apache.tapestry.spec.AssetType;
+import org.apache.tapestry.spec.BindingType;
+import org.apache.tapestry.spec.IAssetSpecification;
+import org.apache.tapestry.spec.IBindingSpecification;
+import org.apache.tapestry.spec.IComponentSpecification;
+import org.apache.tapestry.spec.IContainedComponent;
+import org.apache.tapestry.spec.IListenerBindingSpecification;
+import org.apache.tapestry.spec.IPropertySpecification;
+
+/**
+ *  Runs the process of building the component hierarchy for an entire page.
+ * 
+ *  <p>
+ *  This class is not threadsafe; however, {@link org.apache.tapestry.pageload.PageSource}
+ *  creates a new instance of it for each page to be loaded, which bypasses
+ *  multithreading issues.
+ *
+ *  @author Howard Lewis Ship
+ * 
+ **/
+
+public class PageLoader implements IPageLoader
+{
+    private static final Log LOG = LogFactory.getLog(PageLoader.class);
+
+    private IEngine _engine;
+    private IResourceResolver _resolver;
+    private IComponentClassEnhancer _enhancer;
+    private ISpecificationSource _specificationSource;
+    private ComponentSpecificationResolver _componentResolver;
+    private List _inheritedBindingQueue = new ArrayList();
+    private List _propertyInitializers = new ArrayList();
+    private ComponentTreeWalker _establishDefaultParameterValuesWalker;
+    private ComponentTreeWalker _verifyRequiredParametersWalker;
+
+    /**
+     * The locale of the application, which is also the locale
+     * of the page being loaded.
+     *
+     **/
+
+    private Locale _locale;
+
+    /**
+     *  Number of components instantiated, excluding the page itself.
+     *
+     **/
+
+    private int _count;
+
+    /**
+     *  The recursion depth.  A page with no components is zero.  A component on
+     *  a page is one.
+     *
+     **/
+
+    private int _depth;
+
+    /**
+     *  The maximum depth reached while building the page.
+     *
+     **/
+
+    private int _maxDepth;
+
+    /**
+     *  Used to figure relative paths for context assets.
+     * 
+     **/
+
+    private IResourceLocation _servletLocation;
+
+    private static interface IQueuedInheritedBinding
+    {
+        void connect();
+    }
+
+    private static class QueuedInheritedBinding implements IQueuedInheritedBinding
+    {
+        private IComponent _component;
+        private String _containerParameterName;
+        private String _parameterName;
+
+        private QueuedInheritedBinding(
+            IComponent component,
+            String containerParameterName,
+            String parameterName)
+        {
+            _component = component;
+            _containerParameterName = containerParameterName;
+            _parameterName = parameterName;
+        }
+
+        public void connect()
+        {
+            IBinding binding = _component.getContainer().getBinding(_containerParameterName);
+
+            if (binding == null)
+                return;
+
+            _component.setBinding(_parameterName, binding);
+        }
+    }
+
+    private static class QueuedInheritInformalBindings implements IQueuedInheritedBinding
+    {
+        private IComponent _component;
+
+        private QueuedInheritInformalBindings(IComponent component)
+        {
+            _component = component;
+        }
+
+        public void connect()
+        {
+
+            IComponent container = _component.getContainer();
+
+            for (Iterator it = container.getBindingNames().iterator(); it.hasNext();)
+            {
+                String bindingName = (String) it.next();
+                connectInformalBinding(container, _component, bindingName);
+            }
+        }
+
+        private void connectInformalBinding(
+            IComponent container,
+            IComponent component,
+            String bindingName)
+        {
+            IComponentSpecification componentSpec = component.getSpecification();
+            IComponentSpecification containerSpec = container.getSpecification();
+
+            // check if binding already exists in the component
+            if (component.getBinding(bindingName) != null)
+                return;
+
+            // check if parameter is informal for the component
+            if (componentSpec.getParameter(bindingName) != null
+                || componentSpec.isReservedParameterName(bindingName))
+                return;
+
+            // check if parameter is informal for the container
+            if (containerSpec.getParameter(bindingName) != null
+                || containerSpec.isReservedParameterName(bindingName))
+                return;
+
+            // if everything passes, establish binding
+            IBinding binding = container.getBinding(bindingName);
+            component.setBinding(bindingName, binding);
+        }
+    }
+
+    /**
+     *  Constructor.
+     *
+     **/
+
+    public PageLoader(IRequestCycle cycle)
+    {
+        IEngine engine = cycle.getEngine();
+
+        _specificationSource = engine.getSpecificationSource();
+        _resolver = engine.getResourceResolver();
+        _enhancer = engine.getComponentClassEnhancer();
+        _componentResolver = new ComponentSpecificationResolver(cycle);
+
+        RequestContext context = cycle.getRequestContext();
+
+        // Need the location of the servlet within the context as the basis
+        // for building relative context asset paths.
+
+        HttpServletRequest request = context.getRequest();
+
+        String servletPath = request.getServletPath();
+
+        _servletLocation =
+            new ContextResourceLocation(context.getServlet().getServletContext(), servletPath);
+
+        // Create the mechanisms for walking the component tree when it is complete
+        IComponentVisitor verifyRequiredParametersVisitor = new VerifyRequiredParametersVisitor();
+        _verifyRequiredParametersWalker = new ComponentTreeWalker(new IComponentVisitor[] { verifyRequiredParametersVisitor });
+
+        IComponentVisitor establishDefaultParameterValuesVisitor =
+            new EstablishDefaultParameterValuesVisitor(_resolver);
+        _establishDefaultParameterValuesWalker = new ComponentTreeWalker(new IComponentVisitor[] { establishDefaultParameterValuesVisitor });
+    }
+
+    /**
+     *  Binds properties of the component as defined by the container's specification.
+     *
+     * <p>This implementation is very simple, we will need a lot more
+     *  sanity checking and eror checking in the final version.
+     *
+     *  @param container The containing component.  For a dynamic
+     *  binding ({@link ExpressionBinding}) the property name
+     *  is evaluated with the container as the root.
+     *  @param component The contained component being bound.
+     *  @param spec The specification of the contained component.
+     *  @param contained The contained component specification (from the container's
+     *  {@link IComponentSpecification}).
+     *
+     **/
+
+    private void bind(IComponent container, IComponent component, IContainedComponent contained)
+    {
+        IComponentSpecification spec = component.getSpecification();
+        boolean formalOnly = !spec.getAllowInformalParameters();
+
+        IComponentSpecification containerSpec = container.getSpecification();
+        boolean containerFormalOnly = !containerSpec.getAllowInformalParameters();
+
+        if (contained.getInheritInformalParameters())
+        {
+            if (formalOnly)
+                throw new ApplicationRuntimeException(
+                    Tapestry.format(
+                        "PageLoader.inherit-informal-invalid-component-formal-only",
+                        component.getExtendedId()),
+                    component,
+                    contained.getLocation(),
+                    null);
+
+            if (containerFormalOnly)
+                throw new ApplicationRuntimeException(
+                    Tapestry.format(
+                        "PageLoader.inherit-informal-invalid-container-formal-only",
+                        container.getExtendedId(),
+                        component.getExtendedId()),
+                    component,
+                    contained.getLocation(),
+                    null);
+
+            IQueuedInheritedBinding queued = new QueuedInheritInformalBindings(component);
+            _inheritedBindingQueue.add(queued);
+        }
+
+        Iterator i = contained.getBindingNames().iterator();
+
+        while (i.hasNext())
+        {
+            String name = (String) i.next();
+
+            boolean isFormal = spec.getParameter(name) != null;
+
+            IBindingSpecification bspec = contained.getBinding(name);
+
+            // If not allowing informal parameters, check that each binding matches
+            // a formal parameter.
+
+            if (formalOnly && !isFormal)
+                throw new ApplicationRuntimeException(
+                    Tapestry.format(
+                        "PageLoader.formal-parameters-only",
+                        component.getExtendedId(),
+                        name),
+                    component,
+                    bspec.getLocation(),
+                    null);
+
+            // If an informal parameter that conflicts with a reserved name, then
+            // skip it.
+
+            if (!isFormal && spec.isReservedParameterName(name))
+                continue;
+
+            // The type determines how to interpret the value:
+            // As a simple static String
+            // As a nested property name (relative to the component)
+            // As the name of a binding inherited from the containing component.
+            // As the name of a public field
+            // As a script for a listener
+
+            BindingType type = bspec.getType();
+
+            // For inherited bindings, defer until later.  This gives components
+            // a chance to setup bindings from static values and expressions in the
+            // template.  The order of operations is tricky, template bindings come
+            // later.
+
+            if (type == BindingType.INHERITED)
+            {
+                QueuedInheritedBinding queued =
+                    new QueuedInheritedBinding(component, bspec.getValue(), name);
+                _inheritedBindingQueue.add(queued);
+                continue;
+            }
+
+            if (type == BindingType.LISTENER)
+            {
+                constructListenerBinding(component, name, (IListenerBindingSpecification) bspec);
+                continue;
+            }
+
+            IBinding binding = convert(container, bspec);
+
+            if (binding != null)
+                component.setBinding(name, binding);
+        }
+    }
+
+    private IBinding convert(IComponent container, IBindingSpecification spec)
+    {
+        BindingType type = spec.getType();
+        ILocation location = spec.getLocation();
+        String value = spec.getValue();
+
+        // The most common type. 
+        // TODO These bindings should be created somehow using the SpecFactory in SpecificationParser
+        if (type == BindingType.DYNAMIC)
+            return new ExpressionBinding(_resolver, container, value, location);
+
+        // String bindings are new in 2.0.4.  For the momement,
+        // we don't even try to cache and share them ... they
+        // are most often unique within a page.
+
+        if (type == BindingType.STRING)
+            return new StringBinding(container, value, location);
+
+        // static and field bindings are pooled.  This allows the
+        // same instance to be used with many components.
+
+        if (type == BindingType.STATIC)
+            return new StaticBinding(value, location);
+
+        // BindingType.FIELD is on the way out, it is in the
+        // 1.3 DTD but not the 1.4 DTD.
+
+        if (type == BindingType.FIELD)
+            return new FieldBinding(_resolver, value, location);
+
+        // This code is unreachable, at least until a new type
+        // of binding is created.
+
+        throw new ApplicationRuntimeException("Unexpected type: " + type + ".");
+    }
+
+    /**
+     *  Construct a {@link ListenerBinding} for the component, and add it.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    private void constructListenerBinding(
+        IComponent component,
+        String bindingName,
+        IListenerBindingSpecification spec)
+    {
+        String language = spec.getLanguage();
+
+        // If not provided in the page or component specification, then
+        // search for a default (factory default is "jython").
+
+        if (Tapestry.isBlank(language))
+            language =
+                _engine.getPropertySource().getPropertyValue(
+                    "org.apache.tapestry.default-script-language");
+
+        // Construct the binding.  The first parameter is the compononent
+        // (not the DirectLink or Form, but the page or component containing the link or form).
+
+        IBinding binding =
+            new ListenerBinding(
+                component.getContainer(),
+                language,
+                spec.getScript(),
+                spec.getLocation());
+
+        component.setBinding(bindingName, binding);
+    }
+
+    /**
+     *  Sets up a component.  This involves:
+     *  <ul>
+     * <li>Instantiating any contained components.
+     * <li>Add the contained components to the container.
+     * <li>Setting up bindings between container and containees.
+     * <li>Construct the containees recursively.
+     * <li>Invoking {@link IComponent#finishLoad(IRequestCycle, IPageLoader, IComponentSpecification)}
+     * </ul>
+     *
+     *  @param cycle the request cycle for which the page is being (initially) constructed
+     *  @param page The page on which the container exists.
+     *  @param container The component to be set up.
+     *  @param containerSpec The specification for the container.
+     *  @param the namespace of the container
+     *
+     **/
+
+    private void constructComponent(
+        IRequestCycle cycle,
+        IPage page,
+        IComponent container,
+        IComponentSpecification containerSpec,
+        INamespace namespace)
+    {
+        _depth++;
+        if (_depth > _maxDepth)
+            _maxDepth = _depth;
+
+        List ids = new ArrayList(containerSpec.getComponentIds());
+        int count = ids.size();
+
+        try
+        {
+            for (int i = 0; i < count; i++)
+            {
+                String id = (String) ids.get(i);
+
+                // Get the sub-component specification from the
+                // container's specification.
+
+                IContainedComponent contained = containerSpec.getComponent(id);
+
+                String type = contained.getType();
+                ILocation location = contained.getLocation();
+
+                _componentResolver.resolve(cycle, namespace, type, location);
+
+                IComponentSpecification componentSpecification =
+                    _componentResolver.getSpecification();
+                INamespace componentNamespace = _componentResolver.getNamespace();
+
+                // Instantiate the contained component.
+
+                IComponent component =
+                    instantiateComponent(
+                        page,
+                        container,
+                        id,
+                        componentSpecification,
+                        componentNamespace,
+                        location);
+
+                // Add it, by name, to the container.
+
+                container.addComponent(component);
+
+                // Set up any bindings in the IContainedComponent specification
+
+                bind(container, component, contained);
+
+                // Now construct the component recusively; it gets its chance
+                // to create its subcomponents and set their bindings.
+
+                constructComponent(
+                    cycle,
+                    page,
+                    component,
+                    componentSpecification,
+                    componentNamespace);
+            }
+
+            addAssets(container, containerSpec);
+
+            // Finish the load of the component; most components (which
+            // subclass BaseComponent) load their templates here.
+            // That may cause yet more components to be created, and more
+            // bindings to be set, so we defer some checking until
+            // later.
+
+            container.finishLoad(cycle, this, containerSpec);
+
+            // Finally, we create an initializer for each
+            // specified property.
+
+            createPropertyInitializers(page, container, containerSpec);
+        }
+        catch (ApplicationRuntimeException ex)
+        {
+            throw ex;
+        }
+        catch (RuntimeException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "PageLoader.unable-to-instantiate-component",
+                    container.getExtendedId(),
+                    ex.getMessage()),
+                container,
+                null,
+                ex);
+        }
+
+        _depth--;
+    }
+
+    /**
+     *  Invoked to create an implicit component (one which is defined in the
+     *  containing component's template, rather that in the containing component's
+     *  specification).
+     * 
+     *  @see org.apache.tapestry.BaseComponentTemplateLoader
+     *  @since 3.0
+     * 
+     **/
+
+    public IComponent createImplicitComponent(
+        IRequestCycle cycle,
+        IComponent container,
+        String componentId,
+        String componentType,
+        ILocation location)
+    {
+        IPage page = container.getPage();
+
+        _componentResolver.resolve(cycle, container.getNamespace(), componentType, location);
+
+        INamespace componentNamespace = _componentResolver.getNamespace();
+        IComponentSpecification spec = _componentResolver.getSpecification();
+
+        IComponent result =
+            instantiateComponent(page, container, componentId, spec, componentNamespace, location);
+
+        container.addComponent(result);
+
+        // Recusively build the component.
+
+        constructComponent(cycle, page, result, spec, componentNamespace);
+
+        return result;
+    }
+
+    /**
+     *  Instantiates a component from its specification. We instantiate
+     *  the component object, then set its specification, page, container and id.
+     *
+     *  @see AbstractComponent
+     * 
+     **/
+
+    private IComponent instantiateComponent(
+        IPage page,
+        IComponent container,
+        String id,
+        IComponentSpecification spec,
+        INamespace namespace,
+        ILocation location)
+    {
+        IComponent result = null;
+        String className = spec.getComponentClassName();
+
+        if (Tapestry.isBlank(className))
+            className = BaseComponent.class.getName();
+
+        Class componentClass = _enhancer.getEnhancedClass(spec, className);
+        String enhancedClassName = componentClass.getName();
+
+        try
+        {
+            result = (IComponent) componentClass.newInstance();
+
+        }
+        catch (ClassCastException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("PageLoader.class-not-component", enhancedClassName),
+                container,
+                spec.getLocation(),
+                ex);
+        }
+        catch (Throwable ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("PageLoader.unable-to-instantiate", enhancedClassName),
+                container,
+                spec.getLocation(),
+                ex);
+        }
+
+        if (result instanceof IPage)
+            throw new ApplicationRuntimeException(
+                Tapestry.format("PageLoader.page-not-allowed", result.getExtendedId()),
+                result,
+                null,
+                null);
+
+        result.setNamespace(namespace);
+        result.setSpecification(spec);
+        result.setPage(page);
+        result.setContainer(container);
+        result.setId(id);
+        result.setLocation(location);
+
+        _count++;
+
+        return result;
+    }
+
+    /**
+     *  Instantitates a page from its specification.
+     *
+     *  @param name the unqualified, simple, name for the page
+     *  @param namespace the namespace containing the page's specification
+     *  @param spec the page's specification
+     * 
+     *  We instantiate the page object, then set its specification,
+     *  names and locale.
+     *
+     *  @see IEngine
+     *  @see ChangeObserver
+     **/
+
+    private IPage instantiatePage(String name, INamespace namespace, IComponentSpecification spec)
+    {
+        IPage result = null;
+
+        String pageName = namespace.constructQualifiedName(name);
+        String className = spec.getComponentClassName();
+        ILocation location = spec.getLocation();
+
+        if (Tapestry.isBlank(className))
+        {
+            if (LOG.isDebugEnabled())
+                LOG.debug(
+                    "Page "
+                        + namespace.constructQualifiedName(name)
+                        + " does not specify a component class.");
+
+            className =
+                _engine.getPropertySource().getPropertyValue(
+                    "org.apache.tapestry.default-page-class");
+
+            if (className == null)
+                className = BasePage.class.getName();
+
+            if (LOG.isDebugEnabled())
+                LOG.debug("Defaulting to class " + className);
+        }
+
+        Class pageClass = _enhancer.getEnhancedClass(spec, className);
+        String enhancedClassName = pageClass.getName();
+
+        try
+        {
+            result = (IPage) pageClass.newInstance();
+
+            result.setNamespace(namespace);
+            result.setSpecification(spec);
+            result.setPageName(pageName);
+            result.setPage(result);
+            result.setLocale(_locale);
+            result.setLocation(location);
+        }
+        catch (ClassCastException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("PageLoader.class-not-page", enhancedClassName),
+                location,
+                ex);
+        }
+        catch (Exception ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("PageLoader.unable-to-instantiate", enhancedClassName),
+                location,
+                ex);
+        }
+
+        return result;
+    }
+
+    /**
+     *  Invoked by the {@link PageSource} to load a specific page.  This
+     *  method is not reentrant ... the PageSource ensures that
+     *  any given instance of PageLoader is loading only a single page at a time.
+     *  The page is immediately attached to the {@link IEngine engine}.
+     *
+     *  @param name the simple (unqualified) name of the page to load
+     *  @param namespace from which the page is to be loaded (used
+     *  when resolving components embedded by the page)
+     *  @param cycle the request cycle the page is 
+     *  initially loaded for (this is used
+     *  to define the locale of the new page, and provide access
+     *  to the corect specification source, etc.).
+     *  @param specification the specification for the page
+     *
+     **/
+
+    public IPage loadPage(
+        String name,
+        INamespace namespace,
+        IRequestCycle cycle,
+        IComponentSpecification specification)
+    {
+        IPage page = null;
+
+        _engine = cycle.getEngine();
+
+        _locale = _engine.getLocale();
+
+        _count = 0;
+        _depth = 0;
+        _maxDepth = 0;
+
+        try
+        {
+            page = instantiatePage(name, namespace, specification);
+
+            page.attach(_engine);
+            
+            // As of 3.0.1, this is done now, rather than after constructing the page and its 
+            // components.
+            
+            page.setRequestCycle(cycle);
+
+            constructComponent(cycle, page, page, specification, namespace);
+
+            // Walk through the complete component tree to set up the default parameter values.
+            _establishDefaultParameterValuesWalker.walkComponentTree(page);
+
+            establishInheritedBindings();
+
+            // Walk through the complete component tree to ensure that required parameters are bound 
+            _verifyRequiredParametersWalker.walkComponentTree(page);
+            
+            establishDefaultPropertyValues();
+        }
+        finally
+        {
+            _locale = null;
+            _engine = null;
+            _inheritedBindingQueue.clear();
+            _propertyInitializers.clear();
+        }
+
+        if (LOG.isDebugEnabled())
+            LOG.debug(
+                "Loaded page "
+                    + page
+                    + " with "
+                    + _count
+                    + " components (maximum depth "
+                    + _maxDepth
+                    + ")");
+
+        return page;
+    }
+
+    private void establishInheritedBindings()
+    {
+        LOG.debug("Establishing inherited bindings");
+
+        int count = _inheritedBindingQueue.size();
+
+        for (int i = 0; i < count; i++)
+        {
+            IQueuedInheritedBinding queued =
+                (IQueuedInheritedBinding) _inheritedBindingQueue.get(i);
+
+            queued.connect();
+        }
+    }
+    
+    private void establishDefaultPropertyValues()
+    {
+        LOG.debug("Setting default property values");
+
+        int count = _propertyInitializers.size();
+
+        for (int i = 0; i < count; i++)
+        {
+            PageDetachListener initializer =
+                (PageDetachListener) _propertyInitializers.get(i);
+
+            initializer.pageDetached(null);
+        }
+    }
+
+    private void addAssets(IComponent component, IComponentSpecification specification)
+    {
+        List names = specification.getAssetNames();
+
+        if (names.isEmpty())
+            return;
+
+        IResourceLocation specLocation = specification.getSpecificationLocation();
+
+        Iterator i = names.iterator();
+
+        while (i.hasNext())
+        {
+            String name = (String) i.next();
+            IAssetSpecification assetSpec = specification.getAsset(name);
+            IAsset asset = convert(name, component, assetSpec, specLocation);
+
+            component.addAsset(name, asset);
+        }
+    }
+
+    /**
+     *  Invoked from 
+     *  {@link #constructComponent(IRequestCycle, IPage, IComponent, IComponentSpecification, INamespace)}
+     *  after {@link IComponent#finishLoad(IRequestCycle, IPageLoader, IComponentSpecification)}
+     *  is invoked.  This iterates over any
+     *  {@link org.apache.tapestry.spec.IPropertySpecification}s for the component,
+     *  create an initializer for each.
+     * 
+     **/
+
+    private void createPropertyInitializers(
+        IPage page,
+        IComponent component,
+        IComponentSpecification spec)
+    {
+        List names = spec.getPropertySpecificationNames();
+        int count = names.size();
+
+        for (int i = 0; i < count; i++)
+        {
+            String name = (String) names.get(i);
+            IPropertySpecification ps = spec.getPropertySpecification(name);
+            String expression = ps.getInitialValue();
+
+            PageDetachListener initializer =
+                new PropertyInitializer(_resolver, component, name, expression, ps.getLocation());
+
+            _propertyInitializers.add(initializer);
+            page.addPageDetachListener(initializer);
+        }
+
+    }
+
+    /**
+     *  Builds an instance of {@link IAsset} from the specification.
+     *
+     **/
+
+    private IAsset convert(
+        String assetName,
+        IComponent component,
+        IAssetSpecification spec,
+        IResourceLocation specificationLocation)
+    {
+        AssetType type = spec.getType();
+        String path = spec.getPath();
+        ILocation location = spec.getLocation();
+
+        if (type == AssetType.EXTERNAL)
+            return new ExternalAsset(path, location);
+
+        if (type == AssetType.PRIVATE)
+        {
+            IResourceLocation baseLocation = specificationLocation;
+
+            // Fudge a special case for private assets with complete paths.  The specificationLocation
+            // can't be used because it is often a ContextResourceLocation,
+            // not a ClasspathResourceLocation.
+
+            if (path.startsWith("/"))
+            {
+                baseLocation = new ClasspathResourceLocation(_resolver, "/");
+                path = path.substring(1);
+            }
+
+            return new PrivateAsset(
+                (ClasspathResourceLocation) findAsset(assetName,
+                    component,
+                    baseLocation,
+                    path,
+                    location),
+                location);
+        }
+
+        return new ContextAsset(
+            (ContextResourceLocation) findAsset(assetName,
+                component,
+                _servletLocation,
+                path,
+                location),
+            location);
+    }
+
+    private IResourceLocation findAsset(
+        String assetName,
+        IComponent component,
+        IResourceLocation baseLocation,
+        String path,
+        ILocation location)
+    {
+        IResourceLocation assetLocation = baseLocation.getRelativeLocation(path);
+        IResourceLocation localizedLocation = assetLocation.getLocalization(_locale);
+
+        if (localizedLocation == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "PageLoader.missing-asset",
+                    assetName,
+                    component.getExtendedId(),
+                    assetLocation),
+                component,
+                location,
+                null);
+
+        return localizedLocation;
+    }
+
+    public IEngine getEngine()
+    {
+        return _engine;
+    }
+
+    public ITemplateSource getTemplateSource()
+    {
+        return _engine.getTemplateSource();
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/pageload/PageSource.java b/tapestry-framework/src/org/apache/tapestry/pageload/PageSource.java
new file mode 100644
index 0000000..d72c25d
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/pageload/PageSource.java
@@ -0,0 +1,280 @@
+//  Copyright 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.tapestry.pageload;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.IPage;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.engine.IMonitor;
+import org.apache.tapestry.engine.IPageSource;
+import org.apache.tapestry.resolver.PageSpecificationResolver;
+import org.apache.tapestry.util.MultiKey;
+import org.apache.tapestry.util.pool.Pool;
+
+/**
+ *  A source for pages for a particular application.  Each application
+ *  should have its own <code>PageSource</code>, storing it into the
+ *  {@link javax.servlet.ServletContext} using a unique key (usually built from
+ *  the application name).
+ *
+ *  <p>The <code>PageSource</code> acts as a pool for {@link IPage} instances.
+ *  Pages are retrieved from the pool using {@link #getPage(IRequestCycle, String, IMonitor)}
+ *  and are later returned to the pool using {@link #releasePage(IPage)}.
+ *
+ *
+ *  <p>TBD: Pooled pages stay forever.  Need a strategy for cleaning up the pool,
+ *  tracking which pages have been in the pool the longest, etc.  A mechanism
+ *  for reporting pool statistics would be useful.
+ *
+ *  @author Howard Lewis Ship
+ * 
+ **/
+
+public class PageSource implements IPageSource
+{
+    /**
+     *  Key used to find PageLoader instances in the Pool.
+     * 
+     **/
+
+    private static final String PAGE_LOADER_POOL_KEY = "org.apache.tapestry.PageLoader";
+
+    /**
+     * Key used to find {@link PageSpecificationResolver} instance
+     * in the pool.
+     */
+
+    private static final String PAGE_SPECIFICATION_RESOLVER_KEY =
+        "org.apache.tapestry.PageSpecificationResolver";
+
+    private IResourceResolver _resolver;
+
+    /**
+     *  The pool of {@link IPage}s.  The key is a {@link MultiKey},
+     *  built from the page name and the page locale.  This is a reference
+     *  to a shared pool.
+     * 
+     *  @see IEngine#getPool()
+     *
+     **/
+
+    private Pool _pool;
+
+    public PageSource(IEngine engine)
+    {
+        _resolver = engine.getResourceResolver();
+        _pool = engine.getPool();
+    }
+
+    public IResourceResolver getResourceResolver()
+    {
+        return _resolver;
+    }
+
+    /**
+     *  Builds a key for a named page in the application's current locale.
+     *
+     **/
+
+    protected MultiKey buildKey(IEngine engine, String pageName)
+    {
+        Object[] keys;
+
+        keys = new Object[] { pageName, engine.getLocale()};
+
+        // Don't make a copy, this array is just for the MultiKey.
+
+        return new MultiKey(keys, false);
+    }
+
+    /**
+     *  Builds a key from an existing page, using the page's name and locale.  This is
+     *  used when storing a page into the pool.
+     *
+     **/
+
+    protected MultiKey buildKey(IPage page)
+    {
+        Object[] keys;
+
+        keys = new Object[] { page.getPageName(), page.getLocale()};
+
+        // Don't make a copy, this array is just for the MultiKey.
+
+        return new MultiKey(keys, false);
+    }
+
+    /**
+     *  Gets the page from a pool, or otherwise loads the page.  This operation
+     *  is threadsafe.
+     *
+     **/
+
+    public IPage getPage(IRequestCycle cycle, String pageName, IMonitor monitor)
+    {
+        IEngine engine = cycle.getEngine();
+        Object key = buildKey(engine, pageName);
+        IPage result = (IPage) _pool.retrieve(key);
+
+        if (result == null)
+        {
+            monitor.pageCreateBegin(pageName);
+
+            // Resolvers are not threadsafe, so we get one from
+            // the pool or create as needed.
+
+            PageSpecificationResolver pageSpecificationResolver =
+                getPageSpecificationResolver(cycle);
+
+            pageSpecificationResolver.resolve(cycle, pageName);
+
+            // Likewise PageLoader
+
+            PageLoader loader = getPageLoader(cycle);
+
+            try
+            {
+                result =
+                    loader.loadPage(
+                        pageSpecificationResolver.getSimplePageName(),
+                        pageSpecificationResolver.getNamespace(),
+                        cycle,
+                        pageSpecificationResolver.getSpecification());
+            }
+            finally
+            {
+                discardPageLoader(loader);
+                discardPageSpecificationResolver(pageSpecificationResolver);
+            }
+
+            monitor.pageCreateEnd(pageName);
+        }
+        else
+        {
+            // The page loader attaches the engine, but a page from
+            // the pool needs to be explicitly attached.
+
+            result.attach(engine);
+            result.setRequestCycle(cycle);
+        }
+
+        return result;
+    }
+
+    /**
+     *  Invoked to obtain an instance of 
+     *  {@link PageLoader}.  An instance if aquired from the pool or,
+     *  if none are available, created fresh.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    protected PageLoader getPageLoader(IRequestCycle cycle)
+    {
+        PageLoader result = (PageLoader) _pool.retrieve(PAGE_LOADER_POOL_KEY);
+
+        if (result == null)
+            result = new PageLoader(cycle);
+
+        return result;
+    }
+
+    /**
+     *  Invoked once the {@link PageLoader} is not
+     *  longer needed; it is then returned to the pool.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    protected void discardPageLoader(PageLoader loader)
+    {
+        _pool.store(PAGE_LOADER_POOL_KEY, loader);
+    }
+
+    /**
+     * Invoked to obtain an instance of {@link PageSpecificationResolver}.
+     * An instance is acquired form the pool or, if none are available,
+     * a new one is instantiated.
+     * 
+     * @since 3.0
+     */
+
+    protected PageSpecificationResolver getPageSpecificationResolver(IRequestCycle cycle)
+    {
+        PageSpecificationResolver result =
+            (PageSpecificationResolver) _pool.retrieve(PAGE_SPECIFICATION_RESOLVER_KEY);
+
+        if (result == null)
+            result = new PageSpecificationResolver(cycle);
+
+        return result;
+    }
+
+    /**
+     * Invoked once the {@link PageSpecificationResolver} is no longer
+     * needed, it is returned to the pool.
+     * 
+     * @since 3.0
+     */
+
+    protected void discardPageSpecificationResolver(PageSpecificationResolver resolver)
+    {
+        _pool.store(PAGE_SPECIFICATION_RESOLVER_KEY, resolver);
+    }
+
+    /**
+     *  Returns the page to the appropriate pool.  Invokes
+     *  {@link IPage#detach()}.
+     *
+     **/
+
+    public void releasePage(IPage page)
+    {
+        Tapestry.clearMethodInvocations();
+
+        page.detach();
+
+        Tapestry.checkMethodInvocation(Tapestry.ABSTRACTPAGE_DETACH_METHOD_ID, "detach()", page);
+
+        _pool.store(buildKey(page), page);
+    }
+
+    /**
+     *  Invoked (during testing primarily) to release the entire pool
+     *  of pages, and the caches of bindings and assets.
+     *
+     **/
+
+    public synchronized void reset()
+    {
+        _pool.clear();
+    }
+
+    public String toString()
+    {
+        ToStringBuilder builder = new ToStringBuilder(this);
+
+        builder.append("pool", _pool);
+        builder.append("resolver", _resolver);
+
+        return builder.toString();
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/pageload/PropertyInitializer.java b/tapestry-framework/src/org/apache/tapestry/pageload/PropertyInitializer.java
new file mode 100644
index 0000000..2d3d9eb
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/pageload/PropertyInitializer.java
@@ -0,0 +1,126 @@
+//  Copyright 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.tapestry.pageload;
+
+import ognl.Ognl;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.event.PageDetachListener;
+import org.apache.tapestry.event.PageEvent;
+import org.apache.tapestry.util.prop.OgnlUtils;
+
+/**
+ *  Given a component, a property and a value, this object will
+ *  reset the property to the value whenever the page
+ *  (containing the component) is detached.  This is related
+ *  to support for {@link org.apache.tapestry.spec.IPropertySpecification}s.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ **/
+
+public class PropertyInitializer implements PageDetachListener
+{
+    private IResourceResolver _resolver;
+    private IComponent _component;
+    private String _propertyName;
+    private String _expression;
+    private boolean _invariant;
+    private Object _value;
+    private ILocation _location;
+
+    public PropertyInitializer(
+        IResourceResolver resolver,
+        IComponent component,
+        String propertyName,
+        String expression,
+        ILocation location)
+    {
+        _resolver = resolver;
+        _component = component;
+        _propertyName = propertyName;
+        _expression = expression;
+        _location = location;
+
+        prepareInvariant();
+    }
+
+    public void prepareInvariant()
+    {
+        _invariant = false;
+
+        try
+        {
+            // If no initial value expression is provided, then read the current
+            // property of the expression.  This may be null, or may be
+            // a value set in finishLoad() (via an abstract accessor).
+
+            if (Tapestry.isBlank(_expression))
+            {
+                _invariant = true;
+                _value = OgnlUtils.get(_propertyName, _resolver, _component);
+            }
+            else
+                if (Ognl.isConstant(_expression, Ognl.createDefaultContext(_component, _resolver)))
+                {
+                    // If the expression is a constant, evaluate it and remember the value 
+                    _invariant = true;
+                    _value = OgnlUtils.get(_expression, _resolver, _component);
+                }
+        }
+        catch (Exception ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "PageLoader.unable-to-initialize-property",
+                    _propertyName,
+                    _component,
+                    ex.getMessage()),
+                _location,
+                ex);
+        }
+    }
+
+    public void pageDetached(PageEvent event)
+    {
+        try
+        {
+            if (_invariant)
+                OgnlUtils.set(_propertyName, _resolver, _component, _value);
+            else
+            {
+                Object value = OgnlUtils.get(_expression, _resolver, _component);
+                OgnlUtils.set(_propertyName, _resolver, _component, value);
+            }
+        }
+        catch (Exception ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "PageLoader.unable-to-initialize-property",
+                    _propertyName,
+                    _component,
+                    ex.getMessage()),
+                _location,
+                ex);
+        }
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/pageload/VerifyRequiredParametersVisitor.java b/tapestry-framework/src/org/apache/tapestry/pageload/VerifyRequiredParametersVisitor.java
new file mode 100644
index 0000000..6368cad
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/pageload/VerifyRequiredParametersVisitor.java
@@ -0,0 +1,61 @@
+//  Copyright 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.tapestry.pageload;
+
+import java.util.Iterator;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.spec.IComponentSpecification;
+import org.apache.tapestry.spec.IParameterSpecification;
+
+/**
+ *  Verify whether all required parameters in the examined component are bound,
+ *  and if they are not, throw an exception.
+ * 
+ *  @author mindbridge
+ *  @version $Id$
+ *  @since 3.0
+ */
+public class VerifyRequiredParametersVisitor implements IComponentVisitor
+{
+    /**
+     * @see org.apache.tapestry.pageload.IComponentVisitor#visitComponent(org.apache.tapestry.IComponent)
+     */
+    public void visitComponent(IComponent component)
+    {
+        IComponentSpecification spec = component.getSpecification();
+
+        Iterator i = spec.getParameterNames().iterator();
+
+        while (i.hasNext())
+        {
+            String name = (String) i.next();
+            IParameterSpecification parameterSpec = spec.getParameter(name);
+
+            if (parameterSpec.isRequired() && component.getBinding(name) == null)
+                throw new ApplicationRuntimeException(
+                    Tapestry.format(
+                        "PageLoader.required-parameter-not-bound",
+                        name,
+                        component.getExtendedId()),
+                    component,
+                    component.getLocation(),
+                    null);
+        }
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/pageload/package.html b/tapestry-framework/src/org/apache/tapestry/pageload/package.html
new file mode 100644
index 0000000..7e5a4db
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/pageload/package.html
@@ -0,0 +1,15 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+<p>Classes used when loading pages (and thier heirarchies of components) from thier
+specifications, as well as organizaing thier templates.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/pages/Exception.css b/tapestry-framework/src/org/apache/tapestry/pages/Exception.css
new file mode 100644
index 0000000..1bcec19
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/pages/Exception.css
@@ -0,0 +1,152 @@
+P  {}

+

+H1  {}

+

+H2  {}

+

+H3  {}

+

+A  {}

+

+A:Visited  {}

+

+A:Active  {}

+

+A:Hover  {}

+

+BODY  {}

+

+TABLE.exception-display TR.even  {

+	top : auto;

+}

+

+TABLE.exception-display TR.odd  {

+	top : auto;

+	background-color : #C0C0FF;

+}

+

+TABLE.exception-display TH  {

+	text-align : right;

+	font-weight : bold;

+}

+

+TABLE.exception-display TD  {

+	text-align : left;

+	width : 100%;	

+}

+

+TABLE.exception-display TR.stack-trace  {

+	font-size : small;

+	font-family : sans-serif;

+	text-align : left;

+}

+

+SPAN.exception-header  {

+	font-size : large;

+	font-weight : bold;

+	color : Red;

+}

+

+SPAN.exception-message {

+	font-weight: bold;

+}

+

+TABLE.request-context-border  {

+	border-width : 1;

+	border-color : Black;

+}

+

+SPAN.request-context-object  {

+	font-size : large;

+	font-family : sans-serif;

+	font-weight : bold;

+	text-align : left;

+}

+

+TR.request-context-section TH  {

+	font-size : medium;

+	font-family : sans-serif;

+	font-weight : bold;

+	text-align : center;

+	color : White;

+	background-color : Blue;

+}

+

+TR.request-context-header TH  {

+	font-size : small;

+	font-family : sans-serif;

+	font-weight : bold;

+	text-align : center;

+	color : White;

+	background-color : Blue;

+}

+

+TABLE.request-context-object TD

+{

+	width: 100%;

+}

+

+TABLE.request-context-object TR.odd TD  {

+	text-align : left;

+	color : Black;

+	background-color : #C0C0FF;

+	width: 100%;

+}

+

+TABLE.request-context-object TR.odd TH  {

+	color : Black;

+	background-color : #C0C0FF;

+	text-align : right;

+}

+

+TABLE.request-context-object TR.even TD  {

+	text-align : left;

+}

+

+TABLE.request-context-object TR.even TH  {

+	text-align : right;

+}

+

+TABLE.request-context-object  {

+	width : 100%;

+}

+

+TABLE.request-context-object TR  {

+	vertical-align : text-top;

+}

+

+UL  {

+	margin-top : 0px;

+	margin-bottom : 0px;

+	margin-left : 20px;

+}

+

+TABLE.exception-display TR.exception-name TD  {

+	font-size : larger;

+	font-weight : bold;

+	text-align : center;

+	background-color : Blue;

+	color : White;

+}

+

+TABLE.exception-display  {

+	width : 100%;

+}

+

+TABLE.exception-display TR.exception-message TD  {

+	border-width : 1;

+	border-color : Black;

+	border-style : solid;

+	padding : 2;

+	text-align : left;

+	font-style : italic;

+}

+

+TABLE.exception-display TR.strack-trace-label TD  {

+	margin : 2;

+	border-width : 1;

+	border-color : Black;

+	border-style : solid;

+	text-align : center;

+}

+

diff --git a/tapestry-framework/src/org/apache/tapestry/pages/Exception.html b/tapestry-framework/src/org/apache/tapestry/pages/Exception.html
new file mode 100644
index 0000000..de41efc
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/pages/Exception.html
@@ -0,0 +1,19 @@
+<!-- $Id$ -->
+
+<span jwcid="@Shell" title="Exception" stylesheet="ognl:assets.stylesheet">
+<body>
+
+<span class="exception-header">
+An exception has occurred.
+</span>
+
+<p>You may continue by <b><a jwcid="restart">restarting</a></b> the session.
+
+<span jwcid="@ExceptionDisplay" exceptions="ognl:exceptions"/>
+
+<p>
+
+<span jwcid="@Delegator" delegate="ognl:requestCycle.requestContext"/>
+
+</body>
+</span>
diff --git a/tapestry-framework/src/org/apache/tapestry/pages/Exception.java b/tapestry-framework/src/org/apache/tapestry/pages/Exception.java
new file mode 100644
index 0000000..d7390c7
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/pages/Exception.java
@@ -0,0 +1,54 @@
+//  Copyright 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.tapestry.pages;
+
+import org.apache.tapestry.html.BasePage;
+import org.apache.tapestry.util.exception.ExceptionAnalyzer;
+import org.apache.tapestry.util.exception.ExceptionDescription;
+
+/**
+ *  Default exception reporting page.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class Exception extends BasePage
+{
+    private ExceptionDescription[] _exceptions;
+
+    public void detach()
+    {
+        _exceptions = null;
+
+        super.detach();
+    }
+
+    public ExceptionDescription[] getExceptions()
+    {
+        return _exceptions;
+    }
+
+    public void setException(Throwable value)
+    {
+        ExceptionAnalyzer analyzer;
+
+        analyzer = new ExceptionAnalyzer();
+
+        _exceptions = analyzer.analyze(value);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/pages/Exception.page b/tapestry-framework/src/org/apache/tapestry/pages/Exception.page
new file mode 100644
index 0000000..f4e8f5b
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/pages/Exception.page
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE page-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<page-specification class="org.apache.tapestry.pages.Exception">
+  
+  <component id="restart" type="ServiceLink">
+    <binding name="service" expression="@org.apache.tapestry.Tapestry@RESTART_SERVICE"/>
+  </component>
+  
+  <private-asset name="stylesheet" resource-path="Exception.css"/>
+</page-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/pages/StaleLink.html b/tapestry-framework/src/org/apache/tapestry/pages/StaleLink.html
new file mode 100644
index 0000000..e1ced61
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/pages/StaleLink.html
@@ -0,0 +1,25 @@
+<!-- $Id$ -->
+
+<span jwcid="@Shell" stylesheet="ognl:assets.stylesheet" title="Stale Link">
+
+<body>
+
+You have clicked on a <i>stale link</i>.  
+
+<p>
+<span jwcid="@Insert" value="ognl:message" class="exception-message">
+Exception message goes here.
+</span>
+
+<p>This is most likely the result of using your
+browser's <b>back</b> button, but can also be an application error.
+
+<p>You may continue by returning to the
+application's
+
+<b>
+<a jwcid="home">home page</a></b>.
+
+</body>
+
+</span>
diff --git a/tapestry-framework/src/org/apache/tapestry/pages/StaleLink.java b/tapestry-framework/src/org/apache/tapestry/pages/StaleLink.java
new file mode 100644
index 0000000..6357455
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/pages/StaleLink.java
@@ -0,0 +1,32 @@
+//  Copyright 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.tapestry.pages;
+
+import org.apache.tapestry.html.BasePage;
+
+/**
+ *  Stores a message (taken from the {@link org.apache.tapestry.StaleLinkException})
+ *  that is displayed as part of the page.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public abstract class StaleLink extends BasePage
+{
+    public abstract void setMessage(String message);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/pages/StaleLink.page b/tapestry-framework/src/org/apache/tapestry/pages/StaleLink.page
new file mode 100644
index 0000000..c55a99d
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/pages/StaleLink.page
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE page-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<page-specification class="org.apache.tapestry.pages.StaleLink">
+  
+  <property-specification name="message" type="java.lang.String"/>
+  
+  <component id="home" type="ServiceLink">
+    <binding name="service" expression="@org.apache.tapestry.Tapestry@HOME_SERVICE"/>
+  </component>
+  
+  <private-asset name="stylesheet" resource-path="Exception.css"/>
+
+</page-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/pages/StaleSession.html b/tapestry-framework/src/org/apache/tapestry/pages/StaleSession.html
new file mode 100644
index 0000000..9fc09ac
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/pages/StaleSession.html
@@ -0,0 +1,19 @@
+<!-- $Id$ -->
+
+<span jwcid="@Shell" title="Stale Session" stylesheet="ognl:assets.stylesheet">
+
+<body>
+Your session has timed out.
+
+<p>Web applications store information about what you are doing on the server.  This information
+is called the <em>session</em>.
+
+<p>Web servers must track many, many sessions.  If you
+are inactive for a long enough time (usually, a few minutes), this information is discarded to
+make room for active users.
+
+<p>At this point you may <b>
+<a jwcid="restart">restart</a></b> the session to continue.
+
+</body>
+</span>
diff --git a/tapestry-framework/src/org/apache/tapestry/pages/StaleSession.page b/tapestry-framework/src/org/apache/tapestry/pages/StaleSession.page
new file mode 100644
index 0000000..911b6f2
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/pages/StaleSession.page
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE page-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<page-specification>
+  
+  <component id="restart" type="ServiceLink">
+    <binding name="service" expression="@org.apache.tapestry.Tapestry@RESTART_SERVICE"/>
+  </component>
+  
+  <private-asset name="stylesheet" resource-path="Exception.css"/>
+</page-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/pages/package.html b/tapestry-framework/src/org/apache/tapestry/pages/package.html
new file mode 100644
index 0000000..aeade4d
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/pages/package.html
@@ -0,0 +1,15 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+Basic pages used for errors, stale links and stale sessions.  These can all be
+overriden in the application specification.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/param/AbstractParameterConnector.java b/tapestry-framework/src/org/apache/tapestry/param/AbstractParameterConnector.java
new file mode 100644
index 0000000..7aa1918
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/param/AbstractParameterConnector.java
@@ -0,0 +1,189 @@
+//  Copyright 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.tapestry.param;
+
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.form.Form;
+import org.apache.tapestry.form.IFormComponent;
+import org.apache.tapestry.spec.Direction;
+import org.apache.tapestry.spec.IParameterSpecification;
+import org.apache.tapestry.util.prop.OgnlUtils;
+
+/**
+ *  Standard implementation of {@link IParameterConnector}.
+ *  Subclasses add in the ability to clear parameters.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.0.3
+ * 
+ **/
+
+public abstract class AbstractParameterConnector implements IParameterConnector
+{
+    private String _parameterName;
+    private String _propertyName;
+    private IBinding _binding;
+    private IComponent _component;
+    private boolean _required;
+    private Object _clearValue;
+    private Direction _direction;
+    private IResourceResolver _resolver;
+
+    /**
+     *  Creates a connector.  In addition, obtains the current value
+     *  of the component property; this value will be used to
+     *  restore the component property.
+     * 
+     **/
+
+    protected AbstractParameterConnector(IComponent component, String parameterName, IBinding binding)
+    {
+        _component = component;
+        _parameterName = parameterName;
+        _binding = binding;
+
+        _resolver = component.getPage().getEngine().getResourceResolver();
+
+        IParameterSpecification pspec = _component.getSpecification().getParameter(_parameterName);
+        _required = pspec.isRequired();
+        _propertyName = pspec.getPropertyName();
+        _direction = pspec.getDirection();
+
+        _clearValue = readCurrentPropertyValue();
+    }
+
+    /** @since 2.2 **/
+
+    private Object readCurrentPropertyValue()
+    {
+        return OgnlUtils.get(_propertyName, _resolver, _component);
+    }
+
+    /**
+     *  Sets the property of the component to the specified value.
+     * 
+     **/
+
+    protected void setPropertyValue(Object value)
+    {
+        OgnlUtils.set(_propertyName, _resolver, _component, value);
+    }
+
+    /**
+     *  Gets the value of the binding.
+     *  @param requiredType if not null, the expected type of the value object.
+     * 
+     * 
+     *  @see IBinding#getObject()
+     *  @see IBinding#getObject(String, Class)
+     **/
+
+    protected Object getBindingValue(Class requiredType)
+    {
+        Object result;
+
+        if (requiredType == null)
+            result = _binding.getObject();
+        else
+            result = _binding.getObject(_parameterName, requiredType);
+
+        return result;
+    }
+
+    protected IBinding getBinding()
+    {
+        return _binding;
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer(super.toString());
+        buffer.append('[');
+        buffer.append(_component.getExtendedId());
+        buffer.append(' ');
+        buffer.append(_parameterName);
+        buffer.append(' ');
+        buffer.append(_binding);
+
+        buffer.append(' ');
+        buffer.append(_direction.getName());
+
+        if (_required)
+            buffer.append(" required");
+
+        buffer.append(']');
+
+        return buffer.toString();
+    }
+
+    /**
+     *  Restores the property to its default value.  For
+     *  {@link Direction#FORM} parameters, extracts the
+     *  property value and sets the binding form it
+     *  (when appropriate).
+     * 
+     **/
+
+    public void resetParameter(IRequestCycle cycle)
+    {
+        if (_direction == Direction.FORM && cycle.isRewinding())
+        {
+            IFormComponent component = (IFormComponent) _component;
+
+            if (!component.isDisabled())
+            {
+                IForm form = Form.get(cycle);
+
+                if (form != null && form.isRewinding())
+                {
+                    Object value = readCurrentPropertyValue();
+
+                    _binding.setObject(value);
+                }
+            }
+        }
+
+        // Either way, clear the value.
+
+        setPropertyValue(_clearValue);
+    }
+
+    /**
+     *  Returns true if the connector should update the property value from
+     *  the binding.  For {@link org.apache.tapestry.spec.Direction#IN}, this
+     *  always returns true.  For {@link org.apache.tapestry.spec.Direction#FORM},
+     *  this returns true only if the request cycle and the active form
+     *  are rewinding.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    protected boolean shouldSetPropertyValue(IRequestCycle cycle)
+    {
+        if (_direction == Direction.IN)
+            return true;
+
+        // Must be FORM
+
+        return !cycle.isRewinding();
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/param/BooleanParameterConnector.java b/tapestry-framework/src/org/apache/tapestry/param/BooleanParameterConnector.java
new file mode 100644
index 0000000..bd32e5a
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/param/BooleanParameterConnector.java
@@ -0,0 +1,57 @@
+//  Copyright 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.tapestry.param;
+
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *   Connector for boolean parameters.
+ * 
+ *   @see IBinding#getBoolean()
+ * 
+ *   @author Howard Lewis Ship
+ *   @version $Id$
+ *   @since 2.0.3
+ * 
+ **/
+
+public class BooleanParameterConnector extends AbstractParameterConnector
+{
+
+    protected BooleanParameterConnector(IComponent component, String parameterName, IBinding binding)
+    {
+        super(component, parameterName, binding);
+    }
+
+    /**
+     *  Invokes {@link IBinding#getBoolean()}, which always
+     *  returns true or false (there is no concept of a null
+     *  value).
+     * 
+     **/
+
+    public void setParameter(IRequestCycle cycle)
+    {
+        if (shouldSetPropertyValue(cycle))
+        {
+            boolean value = getBinding().getBoolean();
+
+            setPropertyValue(value ? Boolean.TRUE : Boolean.FALSE);
+        }
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/param/ConnectedParameterException.java b/tapestry-framework/src/org/apache/tapestry/param/ConnectedParameterException.java
new file mode 100644
index 0000000..968189d
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/param/ConnectedParameterException.java
@@ -0,0 +1,70 @@
+//  Copyright 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.tapestry.param;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.ILocation;
+
+/**
+ *  Identifies exceptions in connected parameters (parameters that
+ *  are automatically assigned to component properties by the framework).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.0.3
+ *
+ **/
+
+public class ConnectedParameterException extends ApplicationRuntimeException
+{
+    private String _parameterName;
+    private String _propertyName;
+
+    public ConnectedParameterException(
+        String message,
+        Object component,
+        String parameterName,
+        String propertyName,
+        Throwable rootCause)
+    {
+        this(message, component, parameterName, propertyName, null, rootCause);
+    }
+
+    /** @since 3.0 **/
+
+    public ConnectedParameterException(
+        String message,
+        Object component,
+        String parameterName,
+        String propertyName,
+        ILocation location,
+        Throwable rootCause)
+    {
+        super(message, location, rootCause);
+
+        _parameterName = parameterName;
+        _propertyName = propertyName;
+    }
+
+    public String getParameterName()
+    {
+        return _parameterName;
+    }
+
+    public String getPropertyName()
+    {
+        return _propertyName;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/param/DoubleParameterConnector.java b/tapestry-framework/src/org/apache/tapestry/param/DoubleParameterConnector.java
new file mode 100644
index 0000000..ac76e30
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/param/DoubleParameterConnector.java
@@ -0,0 +1,54 @@
+//  Copyright 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.tapestry.param;
+
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  Connects a parameter to a property of type double.
+ * 
+ *  @author Howard Lewis Ship 
+ *  @version $Id$
+ *  @since 2.0.3
+ * 
+ **/
+
+public class DoubleParameterConnector extends AbstractParameterConnector
+{
+
+    protected DoubleParameterConnector(IComponent component, String parameterName, IBinding binding)
+    {
+        super(component, parameterName, binding);
+    }
+
+    /**
+     *  Invokes {@link IBinding#getDouble()} to obtain the value
+     *  to assign to the property.
+     * 
+     **/
+
+    public void setParameter(IRequestCycle cycle)
+    {
+        if (shouldSetPropertyValue(cycle))
+        {
+            double scalar = getBinding().getDouble();
+
+            setPropertyValue(new Double(scalar));
+        }
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/param/IParameterConnector.java b/tapestry-framework/src/org/apache/tapestry/param/IParameterConnector.java
new file mode 100644
index 0000000..87be635
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/param/IParameterConnector.java
@@ -0,0 +1,51 @@
+//  Copyright 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.tapestry.param;
+
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  Define a type of connector between a binding of a component and a JavaBeans
+ *  property of the component (with the same name).  Allows
+ *  for the parameter to be set before the component is rendered,
+ *  then cleared after the component is rendered.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.0.3
+ * 
+ **/
+
+public interface IParameterConnector
+{
+    /**
+     *  Sets the parameter from the binding.
+     *  
+     *  @throws RequiredParameterException if the parameter is
+     *  required, but the {@link org.apache.tapestry.IBinding}
+     *  supplies a null value.
+     * 
+     **/
+    
+	public void setParameter(IRequestCycle cycle);
+	
+	/**
+	 *  Clears the parameters to a null, 0 or false value
+	 *  (depending on type).
+	 * 
+	 **/
+	
+	public void resetParameter(IRequestCycle cycle);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/param/IntParameterConnector.java b/tapestry-framework/src/org/apache/tapestry/param/IntParameterConnector.java
new file mode 100644
index 0000000..6e9950e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/param/IntParameterConnector.java
@@ -0,0 +1,54 @@
+//  Copyright 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.tapestry.param;
+
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  Connects a parameter to an int property.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.0.3
+ *
+ **/
+
+public class IntParameterConnector extends AbstractParameterConnector
+{
+
+    protected IntParameterConnector(IComponent component, String parameterName, IBinding binding)
+    {
+        super(component, parameterName, binding);
+    }
+
+    /**
+     *  Invokes {@link IBinding#getInt()} to obtain
+     *  an int value to assign.
+     * 
+     **/
+
+    public void setParameter(IRequestCycle cycle)
+    {
+        if (shouldSetPropertyValue(cycle))
+        {
+            int scalar = getBinding().getInt();
+
+            setPropertyValue(new Integer(scalar));
+        }
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/param/ObjectParameterConnector.java b/tapestry-framework/src/org/apache/tapestry/param/ObjectParameterConnector.java
new file mode 100644
index 0000000..1c02003
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/param/ObjectParameterConnector.java
@@ -0,0 +1,55 @@
+//  Copyright 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.tapestry.param;
+
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  Implements {@link IParameterConnector} for object parameters.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.0.3
+ *
+ **/
+
+public class ObjectParameterConnector extends AbstractParameterConnector
+{
+    private Class _requiredType;
+
+    protected ObjectParameterConnector(
+        IComponent component,
+        String parameterName,
+        IBinding binding,
+        Class requiredType)
+    {
+        super(component, parameterName, binding);
+
+        _requiredType = requiredType;
+    }
+
+    /**
+     *  Sets the parameter property to null.
+     * 
+     **/
+
+    public void setParameter(IRequestCycle cycle)
+    {
+        if (shouldSetPropertyValue(cycle))
+            setPropertyValue(getBindingValue(_requiredType));
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/param/ParameterManager.java b/tapestry-framework/src/org/apache/tapestry/param/ParameterManager.java
new file mode 100644
index 0000000..d7ab3c9
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/param/ParameterManager.java
@@ -0,0 +1,355 @@
+//  Copyright 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.tapestry.param;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.BindingException;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.spec.Direction;
+import org.apache.tapestry.spec.IComponentSpecification;
+import org.apache.tapestry.spec.IParameterSpecification;
+import org.apache.tapestry.util.prop.PropertyFinder;
+import org.apache.tapestry.util.prop.PropertyInfo;
+
+/**
+ *  Manages a set of {@link IParameterConnector}s for a
+ *  {@link IComponent}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.0.3
+ *
+ **/
+
+public class ParameterManager
+{
+    private static final Log LOG = LogFactory.getLog(ParameterManager.class);
+
+    /**
+     *  Special types that aren't resolved by class lookups, including
+     *  scalars, arrays of scalars, etc.
+     * 
+     *  <p>
+     *  There's some overlap here with ComponentClassFactory.
+     * 
+     **/
+
+    private static final Map SPECIAL_TYPE_MAP = new HashMap();
+
+    static {
+        SPECIAL_TYPE_MAP.put("boolean", boolean.class);
+        SPECIAL_TYPE_MAP.put("boolean[]", boolean[].class);
+        SPECIAL_TYPE_MAP.put("byte", byte.class);
+        SPECIAL_TYPE_MAP.put("byte[]", byte[].class);
+        SPECIAL_TYPE_MAP.put("char", char.class);
+        SPECIAL_TYPE_MAP.put("char[]", char[].class);
+        SPECIAL_TYPE_MAP.put("short", short.class);
+        SPECIAL_TYPE_MAP.put("short[]", short[].class);
+        SPECIAL_TYPE_MAP.put("int", int.class);
+        SPECIAL_TYPE_MAP.put("int[]", int[].class);
+        SPECIAL_TYPE_MAP.put("long", long.class);
+        SPECIAL_TYPE_MAP.put("long[]", long[].class);
+        SPECIAL_TYPE_MAP.put("float", float.class);
+        SPECIAL_TYPE_MAP.put("float[]", float[].class);
+        SPECIAL_TYPE_MAP.put("double", double.class);
+        SPECIAL_TYPE_MAP.put("double[]", double[].class);
+
+        SPECIAL_TYPE_MAP.put("java.lang.Object[]", Object[].class);
+        SPECIAL_TYPE_MAP.put("java.lang.String[]", String[].class);
+    }
+
+    private IComponent _component;
+    private IParameterConnector[] _connectors;
+
+    public ParameterManager(IComponent component)
+    {
+        _component = component;
+    }
+
+    /**
+     *  Invoked just before a component renders.  Converts bindings to values
+     *  that are assigned to connected properties.
+     * 
+     **/
+
+    public void setParameters(IRequestCycle cycle)
+    {
+        if (_connectors == null)
+            setup(cycle);
+
+        for (int i = 0; i < _connectors.length; i++)
+            _connectors[i].setParameter(cycle);
+    }
+
+    /**
+     *  Invoked just after the component renders.  Returns component properties
+     *  back to initial values (unless the corresponding binding is
+     *  {@link IBinding#isInvariant() invariant}).  In addition, for
+     *  {@link Direction#FORM} parameters, the property is read and the binding
+     *  is set from the property value (if the cycle is rewinding and the current
+     *  form is rewinding).
+     * 
+     **/
+
+    public void resetParameters(IRequestCycle cycle)
+    {
+        if (_connectors == null)
+            return;
+
+        for (int i = 0; i < _connectors.length; i++)
+            _connectors[i].resetParameter(cycle);
+    }
+
+    private void setup(IRequestCycle cycle)
+    {
+        boolean debug = LOG.isDebugEnabled();
+
+        if (debug)
+            LOG.debug(_component + ": connecting parameters and properties");
+
+        List list = new ArrayList();
+        IComponentSpecification spec = _component.getSpecification();
+        IResourceResolver resolver = _component.getPage().getEngine().getResourceResolver();
+
+        IParameterConnector disabledConnector = null;
+
+        Collection names = spec.getParameterNames();
+        Iterator i = names.iterator();
+        while (i.hasNext())
+        {
+            String name = (String) i.next();
+
+            if (debug)
+                LOG.debug("Connecting parameter " + name + ".");
+
+            IBinding binding = _component.getBinding(name);
+            if (binding == null)
+            {
+                if (debug)
+                    LOG.debug("Not bound.");
+
+                continue;
+            }
+
+            IParameterSpecification pspec = spec.getParameter(name);
+            Direction direction = pspec.getDirection();
+
+            if (direction != Direction.IN && direction != Direction.FORM)
+            {
+                if (debug)
+                    LOG.debug("Parameter is " + pspec.getDirection().getName() + ".");
+
+                continue;
+            }
+
+            if (!direction.getAllowInvariant() && binding.isInvariant())
+                throw new ConnectedParameterException(
+                    Tapestry.format(
+                        "ParameterManager.incompatible-direction-and-binding",
+                        new Object[] {
+                            name,
+                            _component.getExtendedId(),
+                            direction.getDisplayName(),
+                            binding }),
+                    _component,
+                    name,
+                    null,
+                    binding.getLocation(),
+                    null);
+
+            String propertyName = pspec.getPropertyName();
+
+            if (debug && !name.equals(propertyName))
+                LOG.debug("Connecting to property " + propertyName + ".");
+
+            // Next,verify that there is a writable property with the same
+            // name as the parameter.
+
+            PropertyInfo propertyInfo =
+                PropertyFinder.getPropertyInfo(_component.getClass(), propertyName);
+
+            if (propertyInfo == null)
+            {
+                throw new ConnectedParameterException(
+                    Tapestry.format(
+                        "ParameterManager.no-accessor",
+                        _component.getExtendedId(),
+                        propertyName),
+                    _component,
+                    name,
+                    propertyName,
+                    binding.getLocation(),
+                    null);
+            }
+
+            if (!propertyInfo.isReadWrite())
+            {
+                throw new ConnectedParameterException(
+                    Tapestry.format(
+                        "ParameterManager.property-not-read-write",
+                        _component.getExtendedId(),
+                        propertyName),
+                    _component,
+                    name,
+                    propertyName,
+                    binding.getLocation(),
+                    null);
+            }
+
+            // Check if the parameter type matches the property type
+
+            Class propertyType = propertyInfo.getType();
+            Class parameterType = getType(pspec.getType(), resolver);
+
+            if (parameterType == null)
+            {
+                throw new ConnectedParameterException(
+                    Tapestry.format(
+                        "ParameterManager.java-type-not-specified",
+                        name,
+                        _component.getExtendedId()),
+                    _component,
+                    name,
+                    propertyName,
+                    binding.getLocation(),
+                    null);
+            }
+
+            if (!propertyType.equals(parameterType))
+            {
+                throw new ConnectedParameterException(
+                    Tapestry.format(
+                        "ParameterManager.type-mismatch",
+                        new String[] {
+                            name,
+                            _component.getExtendedId(),
+                            parameterType.toString(),
+                            propertyType.toString()}),
+                    _component,
+                    name,
+                    propertyName,
+                    binding.getLocation(),
+                    null);
+            }
+
+            // Here's where we will sniff it for type, for the moment
+            // assume its some form of object (not scalar) type.
+
+            IParameterConnector connector =
+                createConnector(_component, name, binding, propertyType, parameterType);
+
+            // Static bindings are set here and then forgotten
+            // about.  Dynamic bindings are kept for later.
+
+            if (binding.isInvariant())
+            {
+                if (debug)
+                    LOG.debug("Setting invariant value using " + connector + ".");
+
+                try
+                {
+                    connector.setParameter(cycle);
+                }
+                catch (BindingException ex)
+                {
+                    throw new ConnectedParameterException(
+                        Tapestry.format(
+                            "ParameterManager.static-initialization-failure",
+                            propertyName,
+                            _component.getExtendedId(),
+                            binding.toString()),
+                        _component,
+                        name,
+                        propertyName,
+                        ex);
+                }
+
+                continue;
+            }
+
+            if (debug)
+                LOG.debug("Adding " + connector + ".");
+
+            // To properly support forms elements, the disabled parameter
+            // must always be processed last.
+
+            if (name.equals("disabled"))
+                disabledConnector = connector;
+            else
+                list.add(connector);
+
+        }
+
+        if (disabledConnector != null)
+            list.add(disabledConnector);
+
+        // Convert for List to array
+
+        _connectors = (IParameterConnector[]) list.toArray(new IParameterConnector[list.size()]);
+
+    }
+
+    private IParameterConnector createConnector(
+        IComponent component,
+        String parameterName,
+        IBinding binding,
+        Class propertyType,
+        Class requiredType)
+    {
+        // Could convert this code to use a Decorator, but then I'd need
+        // some kind of factory for these parameter connectors.
+
+        if (propertyType.equals(Boolean.TYPE))
+            return new BooleanParameterConnector(component, parameterName, binding);
+
+        if (propertyType.equals(Integer.TYPE))
+            return new IntParameterConnector(component, parameterName, binding);
+
+        if (propertyType.equals(Double.TYPE))
+            return new DoubleParameterConnector(component, parameterName, binding);
+
+        if (propertyType.equals(String.class))
+            return new StringParameterConnector(component, parameterName, binding);
+
+        // The default is for any kind of object type
+
+        return new ObjectParameterConnector(component, parameterName, binding, requiredType);
+    }
+
+    private Class getType(String name, IResourceResolver resolver)
+    {
+        if (Tapestry.isBlank(name))
+            return null;
+
+        Class result = (Class) SPECIAL_TYPE_MAP.get(name);
+
+        if (result != null)
+            return result;
+
+        return resolver.findClass(name);
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/param/StringParameterConnector.java b/tapestry-framework/src/org/apache/tapestry/param/StringParameterConnector.java
new file mode 100644
index 0000000..e636ad1
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/param/StringParameterConnector.java
@@ -0,0 +1,52 @@
+//  Copyright 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.tapestry.param;
+
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  Creates a connection between a parameter and a property of type {@link String}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.0.3
+ *
+ **/
+
+public class StringParameterConnector extends AbstractParameterConnector
+{
+
+    protected StringParameterConnector(
+        IComponent component,
+        String parameterName,
+        IBinding binding)
+    {
+        super(component, parameterName, binding);
+    }
+
+    /**
+     *  Invokes {@link IBinding#getString()} to obtain the property
+     *  value.
+     * 
+     **/
+
+    public void setParameter(IRequestCycle cycle)
+    {
+        if (shouldSetPropertyValue(cycle))
+            setPropertyValue(getBinding().getString());
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/param/package.html b/tapestry-framework/src/org/apache/tapestry/param/package.html
new file mode 100644
index 0000000..1a44a94
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/param/package.html
@@ -0,0 +1,15 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+Code to assist {@link org.apache.tapestry.IComponent} in setting JavaBeans properties 
+from bound parameters.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/AbstractSpecificationRule.java b/tapestry-framework/src/org/apache/tapestry/parse/AbstractSpecificationRule.java
new file mode 100644
index 0000000..89e30e6
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/AbstractSpecificationRule.java
@@ -0,0 +1,82 @@
+//  Copyright 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.tapestry.parse;
+
+import org.apache.commons.beanutils.PropertyUtils;
+import org.apache.commons.digester.Rule;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.Tapestry;
+import org.xml.sax.Attributes;
+
+/**
+ *  Placeholder for utility methods needed by the various
+ *  specification-oriented {@link org.apache.commons.digester.Rule}s.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public abstract class AbstractSpecificationRule extends Rule
+{
+
+    protected String getValue(Attributes attributes, String name)
+    {
+        int count = attributes.getLength();
+
+        for (int i = 0; i < count; i++)
+        {
+        	String attributeName = attributes.getLocalName(i);
+        	
+        	if (Tapestry.isBlank(attributeName))
+        		attributeName = attributes.getQName(i);
+        	
+            if (attributeName.equals(name))
+                return attributes.getValue(i);
+        }
+
+        return null;
+    }
+    
+    protected void setProperty(String propertyName, Object value)
+    throws Exception
+    {
+    	PropertyUtils.setProperty(digester.peek(), propertyName, value);
+    }
+
+    /**
+     *  Gets the current location tag.  This requires that the
+     *  rule's digester be {@link SpecificationDigester}.
+     * 
+     **/
+
+    protected ILocation getLocation()
+    {
+        SpecificationDigester locatableDigester = (SpecificationDigester) digester;
+
+        return locatableDigester.getLocationTag();
+    }
+
+    // Temporary, until DocumentParseException is fixed.
+
+    protected IResourceLocation getResourceLocation()
+    {
+        SpecificationDigester locatableDigester = (SpecificationDigester) digester;
+
+        return locatableDigester.getResourceLocation();
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/AttributeType.java b/tapestry-framework/src/org/apache/tapestry/parse/AttributeType.java
new file mode 100644
index 0000000..1724447
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/AttributeType.java
@@ -0,0 +1,70 @@
+//  Copyright 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.tapestry.parse;
+
+import org.apache.commons.lang.enum.Enum;
+
+/**
+ *  The type of an {@link org.apache.tapestry.parse.TemplateAttribute}.
+ *  New types can be created by modifying
+ *  {@link org.apache.tapestry.parse.TemplateParser} to recognize
+ *  the attribute prefix in compnent tags, and
+ *  by modifying
+ *  {@link org.apache.tapestry.BaseComponentTemplateLoader}
+ *  to actually do something with the TemplateAttribute, based on type.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class AttributeType extends Enum
+{
+	/**
+	 *  Indicates the attribute is simple, literal text.  This is
+	 *  the default for any attributes without a recognized
+	 *  prefix.
+	 * 
+	 *  @see org.apache.tapestry.binding.StaticBinding
+	 * 
+	 **/
+	
+	public static final AttributeType LITERAL = new AttributeType("LITERAL");
+
+	/**
+	 *  Indicates the attribute is a OGNL expression.
+	 * 
+	 *  @see org.apache.tapestry.binding.ExpressionBinding
+	 * 
+	 **/
+	
+	public static final AttributeType OGNL_EXPRESSION = new AttributeType("OGNL_EXPRESSION");
+	
+	/**
+	 *  Indicates the attribute is a localization key.
+	 * 
+	 *  @see org.apache.tapestry.binding.StringBinding
+	 * 
+	 **/
+	
+	public static final AttributeType LOCALIZATION_KEY = new AttributeType("LOCALIZATION_KEY");
+
+    private AttributeType(String name)
+    {
+        super(name);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/BaseDocumentRule.java b/tapestry-framework/src/org/apache/tapestry/parse/BaseDocumentRule.java
new file mode 100644
index 0000000..30233b9
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/BaseDocumentRule.java
@@ -0,0 +1,53 @@
+//  Copyright 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.tapestry.parse;
+
+import org.xml.sax.Attributes;
+
+/**
+ *  Base implementation of {@link org.apache.tapestry.parse.IDocumentRule}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+public class BaseDocumentRule implements IDocumentRule
+{
+	private SpecificationDigester _digester;
+	
+	public SpecificationDigester getDigester()
+	{
+		return _digester;
+	}
+	
+    public void setDigester(SpecificationDigester digester)
+    {
+    	_digester = digester;
+    }
+
+    public void startDocument(String namespace, String name, Attributes attributes) throws Exception
+    {
+    }
+
+    public void endDocument() throws Exception
+    {
+    }
+
+    public void finish() throws Exception
+    {
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/BodyRule.java b/tapestry-framework/src/org/apache/tapestry/parse/BodyRule.java
new file mode 100644
index 0000000..56f5895
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/BodyRule.java
@@ -0,0 +1,43 @@
+//  Copyright 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.tapestry.parse;
+
+/**
+ *  Variation of {@link org.apache.commons.digester.BeanPropertySetterRule}
+ *  that does <em>not</em> trim the body text of leading and trailing
+ *  whitespace.  This is important for {@link org.apache.tapestry.spec.IListenerBindingSpecification}s,
+ *  where the whitespace may be relevant!
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class BodyRule extends AbstractSpecificationRule
+{
+	private String _propertyName;
+	
+	public BodyRule(String propertyName)
+	{
+		_propertyName = propertyName;
+	}	
+
+    public void body(String namespace, String name, String text) throws Exception
+    {
+		setProperty(_propertyName, text);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/CloseToken.java b/tapestry-framework/src/org/apache/tapestry/parse/CloseToken.java
new file mode 100644
index 0000000..4b1b42b
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/CloseToken.java
@@ -0,0 +1,51 @@
+//  Copyright 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.tapestry.parse;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.tapestry.ILocation;
+
+/**
+ *  Represents the closing tag of a component element in the template.
+ *
+ *  @see TokenType#CLOSE
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class CloseToken extends TemplateToken
+{
+    private String _tag;
+    
+    public CloseToken(String tag, ILocation location)
+    {
+        super(TokenType.CLOSE, location);
+        
+        _tag = tag;
+    }
+    
+    public String getTag()
+    {
+        return _tag;
+    }
+       
+    protected void extendDescription(ToStringBuilder builder)
+    {
+        builder.append("tag", _tag);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/ComponentCopyOfRule.java b/tapestry-framework/src/org/apache/tapestry/parse/ComponentCopyOfRule.java
new file mode 100644
index 0000000..0877ec8
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/ComponentCopyOfRule.java
@@ -0,0 +1,89 @@
+//  Copyright 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.tapestry.parse;
+
+import java.util.Iterator;
+
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.spec.IBindingSpecification;
+import org.apache.tapestry.spec.IComponentSpecification;
+import org.apache.tapestry.spec.IContainedComponent;
+import org.apache.tapestry.util.xml.DocumentParseException;
+import org.xml.sax.Attributes;
+
+/**
+ *  A rule for processing the copy-of attribute
+ *  of the &lt;component&gt; element (in a page
+ *  or component specification).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class ComponentCopyOfRule extends AbstractSpecificationRule
+{
+    /**
+     *  Validates that the element has either type or copy-of (not both, not neither).
+     *  Uses the copy-of attribute to find a previously declared component
+     *  and copies its type and bindings into the new component (on top of the stack).
+     * 
+     **/
+
+    public void begin(String namespace, String name, Attributes attributes) throws Exception
+    {
+        String id = getValue(attributes, "id");
+        String copyOf = getValue(attributes, "copy-of");
+        String type = getValue(attributes, "type");
+
+        if (Tapestry.isBlank(copyOf))
+        {
+            if (Tapestry.isBlank(type))
+                throw new DocumentParseException(
+                    Tapestry.format("SpecificationParser.missing-type-or-copy-of", id),
+                    getResourceLocation());
+
+            return;
+        }
+
+        if (Tapestry.isNonBlank(type))
+            throw new DocumentParseException(
+                Tapestry.format("SpecificationParser.both-type-and-copy-of", id),
+                getResourceLocation());
+
+        IComponentSpecification spec = (IComponentSpecification) digester.getRoot();
+
+        IContainedComponent source = spec.getComponent(copyOf);
+        if (source == null)
+            throw new DocumentParseException(
+                Tapestry.format("SpecificationParser.unable-to-copy", copyOf),
+                getResourceLocation());
+
+        IContainedComponent target = (IContainedComponent) digester.peek();
+
+        target.setType(source.getType());
+        target.setCopyOf(copyOf);
+
+        Iterator i = source.getBindingNames().iterator();
+        while (i.hasNext())
+        {
+            String bindingName = (String) i.next();
+            IBindingSpecification binding = source.getBinding(bindingName);
+            target.setBinding(bindingName, binding);
+        }
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/ComponentTemplate.java b/tapestry-framework/src/org/apache/tapestry/parse/ComponentTemplate.java
new file mode 100644
index 0000000..1837a0a
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/ComponentTemplate.java
@@ -0,0 +1,74 @@
+//  Copyright 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.tapestry.parse;
+
+/**
+ *  Enapsulates a parsed component template, allowing access to the
+ *  tokens parsed.
+ *
+ *  <p>TBD:  Record the name of the resource (or other location) from which
+ *  the template was parsed (useful during debugging).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class ComponentTemplate
+{
+    /**
+     *  The HTML template from which the tokens were generated.  This is a string
+     *  read from a resource.  The tokens represents offsets and lengths into
+     *  this string.
+     *
+     **/
+
+    private char[] _templateData;
+
+    private TemplateToken[] _tokens;
+
+    /**
+     *  Creates a new ComponentTemplate.
+     *
+     *  @param templateData The template data.  This is <em>not</em> copied, so
+     *  the array passed in should not be modified further.
+     *
+     *  @param tokens  The tokens making up the template.  This is also
+     *  retained (<em>not</em> copied), and so should not
+     *  be modified once passed to the constructor.
+     *
+     **/
+
+    public ComponentTemplate(char[] templateData, TemplateToken[] tokens)
+    {
+        _templateData = templateData;
+        _tokens = tokens;
+    }
+
+    public char[] getTemplateData()
+    {
+        return _templateData;
+    }
+
+    public TemplateToken getToken(int index)
+    {
+        return _tokens[index];
+    }
+
+    public int getTokenCount()
+    {
+        return _tokens.length;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/ConnectChildRule.java b/tapestry-framework/src/org/apache/tapestry/parse/ConnectChildRule.java
new file mode 100644
index 0000000..cf13890
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/ConnectChildRule.java
@@ -0,0 +1,67 @@
+//  Copyright 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.tapestry.parse;
+
+import org.apache.commons.beanutils.MethodUtils;
+import org.xml.sax.Attributes;
+
+/**
+ *  Connects a child object to a parent object using a named method.  The method
+ *  takes two parameters: the name of the child object and the child object itself.
+ *  The child object name is taken from an attribute.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class ConnectChildRule extends AbstractSpecificationRule
+{
+    private String _methodName;
+    private String _attributeName;
+
+    private String _attributeValue;
+
+    public ConnectChildRule(String methodName, String attributeName)
+    {
+        _methodName = methodName;
+        _attributeName = attributeName;
+    }
+
+    public void begin(String namespace, String name, Attributes attributes) throws Exception
+    {
+        _attributeValue = getValue(attributes, _attributeName);
+
+        // Check for null?
+    }
+
+    /**
+     *  Performs the add.  This is done in <code>end()</code> to ensure
+     *  that the child object (on top of the stack) is fully initialized.
+     * 
+     **/
+
+    public void end(String namespace, String name) throws Exception
+    {
+        Object child = digester.peek();
+        Object parent = digester.peek(1);
+
+        MethodUtils.invokeMethod(parent, _methodName, new Object[] { _attributeValue, child });
+
+        _attributeValue = null;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/DisallowFrameworkNamespaceRule.java b/tapestry-framework/src/org/apache/tapestry/parse/DisallowFrameworkNamespaceRule.java
new file mode 100644
index 0000000..b632e5a
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/DisallowFrameworkNamespaceRule.java
@@ -0,0 +1,47 @@
+//  Copyright 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.tapestry.parse;
+
+import org.apache.tapestry.INamespace;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.util.xml.DocumentParseException;
+import org.xml.sax.Attributes;
+
+/**
+ *  Special purpose rule that simple validates that a library does
+ *  not use the reserved framework namespace.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class DisallowFrameworkNamespaceRule extends AbstractSpecificationRule
+{
+
+    public void begin(String namespace, String name, Attributes attributes) throws Exception
+    {
+        String id = getValue(attributes, "id");
+
+        if (id.equals(INamespace.FRAMEWORK_NAMESPACE))
+            throw new DocumentParseException(
+                Tapestry.format(
+                    "SpecificationParser.framework-library-id-is-reserved",
+                    INamespace.FRAMEWORK_NAMESPACE),
+                getResourceLocation());
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/IDocumentRule.java b/tapestry-framework/src/org/apache/tapestry/parse/IDocumentRule.java
new file mode 100644
index 0000000..b7ebd5f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/IDocumentRule.java
@@ -0,0 +1,45 @@
+//  Copyright 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.tapestry.parse;
+
+import org.xml.sax.Attributes;
+
+/**
+ *  A {@link org.apache.tapestry.parse.SpecificationDigester} rule that executes
+ *  at the start and end of the document.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public interface IDocumentRule
+{
+    public void setDigester(SpecificationDigester digester);
+
+	/**
+	 *  Invoked at the time the first element in the document is parsed.
+	 * 
+	 *  By this time, the publicId will be known.
+	 * 
+	 **/
+	
+    public void startDocument(String namespace, String name, Attributes attributes) throws Exception;
+
+    public void endDocument() throws Exception;
+
+    public void finish() throws Exception;
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/ITemplateParserDelegate.java b/tapestry-framework/src/org/apache/tapestry/parse/ITemplateParserDelegate.java
new file mode 100644
index 0000000..01cf54c
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/ITemplateParserDelegate.java
@@ -0,0 +1,63 @@
+//  Copyright 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.tapestry.parse;
+
+import org.apache.tapestry.ILocation;
+
+/**
+ *  Provides a {@link TemplateParser} with additional information about
+ *  dynamic components.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public interface ITemplateParserDelegate
+{
+    /**
+     *  Returns true if the component id is valid, false if the
+     *  component id is not recognized.
+     *
+     **/
+
+    public boolean getKnownComponent(String componentId);
+
+    /**
+     *  Returns true if the specified component allows a body, false
+     *  otherwise.  The parser uses this information to determine
+     *  if it should ignore the body of a tag.
+     *
+     *  @throws org.apache.tapestry.ApplicationRuntimeException if no such component exists
+     * 
+     **/
+
+    public boolean getAllowBody(String componentId, ILocation location);
+
+    /**
+     *  Used with implicit components to determine if the component
+     *  allows a body or not.
+     * 
+     *  @param libraryId the specified library id, possibly null
+     *  @param type the component type
+     * 
+     *  @throws org.apache.tapestry.ApplicationRuntimeException if the specification cannot be found
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    public boolean getAllowBody(String libraryId, String type, ILocation location);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/InitializePropertyRule.java b/tapestry-framework/src/org/apache/tapestry/parse/InitializePropertyRule.java
new file mode 100644
index 0000000..dc76c82
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/InitializePropertyRule.java
@@ -0,0 +1,46 @@
+//  Copyright 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.tapestry.parse;
+
+import org.xml.sax.Attributes;
+
+/**
+ *  Used to initialize a property of an object on the top of the digester stack.
+ *  This should come after the {@link org.apache.commons.digester.ObjectCreateRule}
+ *  (or variation) and before and property setting for the object.  Remember
+ *  that rules order matters with the digester.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class InitializePropertyRule extends AbstractSpecificationRule
+{
+    private String _propertyName;
+    private Object _value;
+
+    public InitializePropertyRule(String propertyName, Object value)
+    {
+        _propertyName = propertyName;
+        _value = value;
+    }
+
+    public void begin(String namespace, String name, Attributes attributes) throws Exception
+    {
+        setProperty(_propertyName, _value);
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/LocalizationToken.java b/tapestry-framework/src/org/apache/tapestry/parse/LocalizationToken.java
new file mode 100644
index 0000000..ba026b9
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/LocalizationToken.java
@@ -0,0 +1,87 @@
+//  Copyright 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.tapestry.parse;
+
+import java.util.Map;
+
+import org.apache.tapestry.ILocation;
+
+/**
+ *  Represents localized text from the template.
+ *
+ *  @see TokenType#LOCALIZATION
+ * 
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class LocalizationToken extends TemplateToken
+{
+    private String _tag;
+    private String _key;
+    private boolean _raw;
+    private Map _attributes;
+    
+    /**
+     *  Creates a new token.
+     * 
+     * 
+     *  @param tag the tag of the element from the template
+     *  @param key the localization key specified
+     *  @param raw if true, then the localized value contains markup that should not be escaped
+     *  @param attributes any additional attributes (beyond those used to define key and raw)
+     *  that were specified.  This value is retained, not copied.
+     *  @param location location of the tag which defines this token
+     * 
+     **/
+    
+    public LocalizationToken(String tag, String key, boolean raw, Map attributes, ILocation location)
+    {
+        super(TokenType.LOCALIZATION, location);
+        
+        _tag = tag;
+        _key = key;
+        _raw = raw;
+        _attributes = attributes;
+    }
+    
+    /**
+     *  Returns any attributes for the token, which may be null.  Do not modify
+     *  the return value.
+     * 
+     **/
+    
+    public Map getAttributes()
+    {
+        return _attributes;
+    }
+
+    public boolean isRaw()
+    {
+        return _raw;
+    }
+
+    public String getTag()
+    {
+        return _tag;
+    }
+
+    public String getKey()
+    {
+        return _key;
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/OpenToken.java b/tapestry-framework/src/org/apache/tapestry/parse/OpenToken.java
new file mode 100644
index 0000000..dfec6ff
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/OpenToken.java
@@ -0,0 +1,128 @@
+//  Copyright 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.tapestry.parse;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.tapestry.ILocation;
+
+/**
+ *  Token representing the open tag for a component.  Components may be either
+ *  specified or implicit.  Specified components (the traditional type, dating
+ *  back to the origin of Tapestry) are matched by an entry in the
+ *  containing component's specification.  Implicit components specify their
+ *  type in the component template and must not have an entry in
+ *  the containing component's specification.
+ *
+ *  @see TokenType#OPEN
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class OpenToken extends TemplateToken
+{
+    private String _tag;
+    private String _id;
+    private String _componentType;
+    private Map _attributes;
+
+    /**
+     *  Creates a new token with the given tag, id and type 
+     * 
+     *  @param tag the template tag which represents the component, typically "span"
+     *  @param id the id for the component, which may be assigned by the template
+     *  parser for implicit components
+     *  @param  componentType the type of component, if an implicit component, or null for
+     *  a specified component
+     *  @param location location of tag represented by this token
+     * 
+     **/
+
+    public OpenToken(String tag, String id, String componentType, ILocation location)
+    {
+        super(TokenType.OPEN, location);
+
+        _tag = tag;
+        _id = id;
+        _componentType = componentType;
+    }
+
+    /**
+     *  Returns the id for the component.
+     * 
+     **/
+    
+    public String getId()
+    {
+        return _id;
+    }
+
+    /**
+     *  Returns the tag used to represent the component within the template.
+     * 
+     **/
+    
+    public String getTag()
+    {
+        return _tag;
+    }
+    
+    /**
+     *  Returns the specified component type, or null for a component where the type
+     *  is not defined in the template.  The type may include a library id prefix.
+     * 
+     **/
+    
+    public String getComponentType()
+    {
+        return _componentType;
+    }
+
+	public void addAttribute(String name, AttributeType type, String value)
+	{
+		TemplateAttribute attribute = new TemplateAttribute(type, value);
+		
+		if (_attributes == null)
+		_attributes = new HashMap();
+		
+		_attributes.put(name, attribute);
+	}
+	
+	/**
+	 *  Returns a Map of attributes.  Key is the attribute name, value
+	 *  is an instance of {@link org.apache.tapestry.parse.TemplateAttribute}.
+	 *  The caller should not modify the Map.  Returns null if
+	 *  this OpenToken contains no attributes.
+	 * 
+	 **/
+	
+	public Map getAttributesMap()
+	{
+		return _attributes;
+	}
+
+    protected void extendDescription(ToStringBuilder builder)
+    {
+        builder.append("id", _id);
+        builder.append("componentType", _componentType);
+        builder.append("tag", _tag);
+        builder.append("attributes", _attributes);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/SetBooleanPropertyRule.java b/tapestry-framework/src/org/apache/tapestry/parse/SetBooleanPropertyRule.java
new file mode 100644
index 0000000..5808b9c
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/SetBooleanPropertyRule.java
@@ -0,0 +1,51 @@
+//  Copyright 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.tapestry.parse;
+
+import org.xml.sax.Attributes;
+
+/**
+ *  Sets a boolean property from an attribute of the current element.
+ *  The value must be either "yes" or "no" (or not present).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+public class SetBooleanPropertyRule extends AbstractSpecificationRule
+{
+    private String _attributeName;
+    private String _propertyName;
+
+    public SetBooleanPropertyRule(String attributeName, String propertyName)
+    {
+        _attributeName = attributeName;
+        _propertyName = propertyName;
+    }
+
+    public void begin(String namespace, String name, Attributes attributes) throws Exception
+    {
+        String attributeValue = getValue(attributes, _attributeName);
+
+        if (attributeValue == null)
+            return;
+
+        Boolean propertyValue = attributeValue.equals("yes") ? Boolean.TRUE : Boolean.FALSE;
+
+        setProperty(_propertyName, propertyValue);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/SetConvertedPropertyRule.java b/tapestry-framework/src/org/apache/tapestry/parse/SetConvertedPropertyRule.java
new file mode 100644
index 0000000..6516971
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/SetConvertedPropertyRule.java
@@ -0,0 +1,59 @@
+//  Copyright 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.tapestry.parse;
+
+import java.util.Map;
+
+import org.xml.sax.Attributes;
+
+/**
+ *  Rule that applies a conversion of a string value from an attribute into
+ *  an object value before assigning it to the property.  This is used
+ *  to translate values from strings to
+ *  {@link org.apache.commons.lang.enum.Enum}s.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class SetConvertedPropertyRule extends AbstractSpecificationRule
+{
+    private Map _map;
+    private String _attributeName;
+    private String _propertyName;
+
+    public SetConvertedPropertyRule(Map map, String attributeName, String propertyName)
+    {
+        _map = map;
+        _attributeName = attributeName;
+        _propertyName = propertyName;
+    }
+
+    public void begin(String namespace, String name, Attributes attributes) throws Exception
+    {
+        String attributeValue = getValue(attributes, _attributeName);
+        if (attributeValue == null)
+            return;
+
+        Object propertyValue = _map.get(attributeValue);
+
+        // Check for null here?
+
+        setProperty(_propertyName, propertyValue);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/SetExtendedPropertyRule.java b/tapestry-framework/src/org/apache/tapestry/parse/SetExtendedPropertyRule.java
new file mode 100644
index 0000000..28964d9
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/SetExtendedPropertyRule.java
@@ -0,0 +1,92 @@
+//  Copyright 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.tapestry.parse;
+
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.util.xml.DocumentParseException;
+import org.xml.sax.Attributes;
+
+/**
+ *  Sets a property from an extended attribute.  An extended attribute
+ *  is a value that may either be specified inside an XML attribute or,
+ *  if the attribute is not present, in the body of the element.
+ *  It is not allowed that the value be specified in both places.
+ *  The value may be optional or required.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class SetExtendedPropertyRule extends AbstractSpecificationRule
+{
+    private String _attributeName;
+    private String _propertyName;
+    private boolean _required;
+
+    private boolean _valueSet;
+
+    public SetExtendedPropertyRule(String attributeName, String propertyName, boolean required)
+    {
+        _attributeName = attributeName;
+        _propertyName = propertyName;
+        _required = required;
+    }
+
+    public void begin(String namespace, String name, Attributes attributes) throws Exception
+    {
+        String value = getValue(attributes, _attributeName);
+
+        if (value != null)
+        {
+            setProperty(_propertyName, value);
+            _valueSet = true;
+        }
+    }
+
+    public void body(String namespace, String name, String text) throws Exception
+    {
+        if (Tapestry.isBlank(text))
+            return;
+
+        if (_valueSet)
+        {
+            throw new DocumentParseException(
+                Tapestry.format(
+                    "SpecificationParser.no-attribute-and-body",
+                    _attributeName,
+                    name),
+                getResourceLocation());
+        }
+
+        setProperty(_propertyName, text.trim());
+        _valueSet = true;
+    }
+
+    public void end(String namespace, String name) throws Exception
+    {
+        if (!_valueSet && _required)
+            throw new DocumentParseException(
+                Tapestry.format(
+                    "SpecificationParser.required-extended-attribute",
+                    name,
+                    _attributeName),
+                getResourceLocation());
+
+        _valueSet = false;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/SetLimitedPropertiesRule.java b/tapestry-framework/src/org/apache/tapestry/parse/SetLimitedPropertiesRule.java
new file mode 100644
index 0000000..4d5ef44
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/SetLimitedPropertiesRule.java
@@ -0,0 +1,77 @@
+//  Copyright 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.tapestry.parse;
+
+import org.apache.commons.beanutils.PropertyUtils;
+import org.apache.tapestry.Tapestry;
+import org.xml.sax.Attributes;
+
+/**
+ *  Much like {@link org.apache.commons.digester.SetPropertiesRule}, but
+ *  only properties that are declared will be copied; other properties
+ *  will be ignored.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+public class SetLimitedPropertiesRule extends AbstractSpecificationRule
+{
+    private String[] _attributeNames;
+    private String[] _propertyNames;
+
+    public SetLimitedPropertiesRule(String attributeName, String propertyName)
+    {
+        this(new String[] { attributeName }, new String[] { propertyName });
+    }
+
+    public SetLimitedPropertiesRule(String[] attributeNames, String[] propertyNames)
+    {
+        _attributeNames = attributeNames;
+        _propertyNames = propertyNames;
+    }
+
+    public void begin(String namespace, String name, Attributes attributes) throws Exception
+    {
+        Object top = digester.peek();
+
+        int count = attributes.getLength();
+
+        for (int i = 0; i < count; i++)
+        {
+            String attributeName = attributes.getLocalName(i);
+
+            if (Tapestry.isBlank(attributeName))
+                attributeName = attributes.getQName(i);
+
+            for (int x = 0; x < _attributeNames.length; x++)
+            {
+                if (_attributeNames[x].equals(attributeName))
+                {
+                    String value = attributes.getValue(i);
+                    String propertyName = _propertyNames[x];
+
+                    PropertyUtils.setProperty(top, propertyName, value);
+
+                    // Terminate inner loop when attribute name is found.
+
+                    break;
+                }
+            }
+        }
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/SetMetaPropertyRule.java b/tapestry-framework/src/org/apache/tapestry/parse/SetMetaPropertyRule.java
new file mode 100644
index 0000000..47f21e4
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/SetMetaPropertyRule.java
@@ -0,0 +1,78 @@
+//  Copyright 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.tapestry.parse;
+
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.util.IPropertyHolder;
+import org.apache.tapestry.util.xml.DocumentParseException;
+import org.xml.sax.Attributes;
+
+/**
+ *  Handles the &lt;property&gt; element in Tapestry specifications, which is 
+ *  designed to hold meta-data about specifications.
+ *  Expects the top object on the stack to be a {@link org.apache.tapestry.util.IPropertyHolder}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class SetMetaPropertyRule extends AbstractSpecificationRule
+{
+    private String _name;
+    private String _value;
+
+    public void begin(String namespace, String name, Attributes attributes) throws Exception
+    {
+        _name = getValue(attributes, "name");
+
+        // First, get the value from the attribute, if present
+
+        _value = getValue(attributes, "value");
+
+    }
+
+    public void body(String namespace, String name, String text) throws Exception
+    {
+        if (Tapestry.isBlank(text))
+            return;
+
+        if (_value != null)
+        {
+            throw new DocumentParseException(
+                Tapestry.format("SpecificationParser.no-attribute-and-body", "value", name),
+                getResourceLocation());
+        }
+
+        _value = text.trim();
+    }
+
+    public void end(String namespace, String name) throws Exception
+    {
+        if (_value == null)
+            throw new DocumentParseException(
+                Tapestry.format("SpecificationParser.required-extended-attribute", name, "value"),
+                getResourceLocation());
+
+        IPropertyHolder holder = (IPropertyHolder) digester.peek();
+
+        holder.setProperty(_name, _value);
+
+        _name = null;
+        _value = null;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/SetPublicIdRule.java b/tapestry-framework/src/org/apache/tapestry/parse/SetPublicIdRule.java
new file mode 100644
index 0000000..ae64893
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/SetPublicIdRule.java
@@ -0,0 +1,37 @@
+//  Copyright 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.tapestry.parse;
+
+import org.xml.sax.Attributes;
+
+/**
+ *  Sets the publicId
+ *  property of the parsed specification.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class SetPublicIdRule extends AbstractSpecificationRule
+{
+
+    public void begin(String namespace, String name, Attributes attributes) throws Exception
+    {
+        setProperty("publicId", digester.getPublicId());
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/SpecificationDigester.java b/tapestry-framework/src/org/apache/tapestry/parse/SpecificationDigester.java
new file mode 100644
index 0000000..9847d6a
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/SpecificationDigester.java
@@ -0,0 +1,300 @@
+//  Copyright 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.tapestry.parse;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.digester.Digester;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.Location;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.util.RegexpMatcher;
+import org.xml.sax.Attributes;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+
+/**
+ *  Extension of {@link org.apache.commons.digester.Digester} with additional rules, hooks
+ *  and methods needed when parsing Tapestry specifications.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class SpecificationDigester extends Digester
+{
+    private List _documentRules;
+    private IResourceLocation _resourceLocation;
+    private RegexpMatcher _matcher;
+
+    private ILocation _lastLocation;
+    private int _lastLine;
+    private int _lastColumn;
+
+    /**
+     *  Notifications are sent with the very first element.
+     * 
+     **/
+
+    private boolean _firstElement = true;
+
+    public void addDocumentRule(IDocumentRule rule)
+    {
+        if (_documentRules == null)
+            _documentRules = new ArrayList();
+
+        rule.setDigester(this);
+
+        _documentRules.add(rule);
+    }
+
+    public void addSetBooleanProperty(String pattern, String attributeName, String propertyName)
+    {
+        addRule(pattern, new SetBooleanPropertyRule(attributeName, propertyName));
+    }
+
+    public void addSetExtendedProperty(
+        String pattern,
+        String attributeName,
+        String propertyName,
+        boolean required)
+    {
+        addRule(pattern, new SetExtendedPropertyRule(attributeName, propertyName, required));
+    }
+
+    public void addValidate(
+        String pattern,
+        String attributeName,
+        String valuePattern,
+        String errorKey)
+    {
+        addRule(pattern, new ValidateRule(getMatcher(), attributeName, valuePattern, errorKey));
+    }
+
+    public void addSetConvertedProperty(
+        String pattern,
+        Map map,
+        String attributeName,
+        String propertyName)
+    {
+        addRule(pattern, new SetConvertedPropertyRule(map, attributeName, propertyName));
+    }
+
+    public void addConnectChild(String pattern, String methodName, String attributeName)
+    {
+        addRule(pattern, new ConnectChildRule(methodName, attributeName));
+    }
+
+    public void addInitializeProperty(String pattern, String propertyName, Object value)
+    {
+        addRule(pattern, new InitializePropertyRule(propertyName, value));
+    }
+
+    public void addSetLimitedProperties(String pattern, String attributeName, String propertyName)
+    {
+        addRule(pattern, new SetLimitedPropertiesRule(attributeName, propertyName));
+    }
+
+    public void addSetLimitedProperties(
+        String pattern,
+        String[] attributeNames,
+        String[] propertyNames)
+    {
+        addRule(pattern, new SetLimitedPropertiesRule(attributeNames, propertyNames));
+    }
+
+    public void addBody(String pattern, String propertyName)
+    {
+        addRule(pattern, new BodyRule(propertyName));
+    }
+
+    /**
+     *  Returns the {@link org.xml.sax.Locator} for the Digester.  This object
+     *  is provided by the underlying SAX parser to identify where in the
+     *  document the parse is currently located; this information can be
+     *  used to build a {@link org.apache.tapestry.ILocation}.
+     * 
+     **/
+
+    public Locator getLocator()
+    {
+        return locator;
+    }
+
+    public ILocation getLocationTag()
+    {
+        int line = -1;
+        int column = -1;
+
+        if (locator != null)
+        {
+            line = locator.getLineNumber();
+            column = locator.getColumnNumber();
+        }
+
+        if (_lastLine != line || _lastColumn != column)
+            _lastLocation = null;
+
+        if (_lastLocation == null)
+        {
+            _lastLine = line;
+            _lastColumn = column;
+            _lastLocation = new Location(_resourceLocation, line, column);
+        }
+
+        return _lastLocation;
+    }
+
+    public IResourceLocation getResourceLocation()
+    {
+        return _resourceLocation;
+    }
+
+    public void setResourceLocation(IResourceLocation resourceLocation)
+    {
+        _resourceLocation = resourceLocation;
+
+        _lastLocation = null;
+        _lastLine = -1;
+        _lastColumn = -1;
+    }
+
+    public RegexpMatcher getMatcher()
+    {
+        if (_matcher == null)
+            _matcher = new RegexpMatcher();
+
+        return _matcher;
+    }
+
+    public void endDocument() throws SAXException
+    {
+        int count = Tapestry.size(_documentRules);
+
+        for (int i = 0; i < count; i++)
+        {
+            IDocumentRule rule = (IDocumentRule) _documentRules.get(i);
+
+            try
+            {
+
+                rule.endDocument();
+            }
+            catch (Exception ex)
+            {
+                throw createSAXException(ex);
+            }
+        }
+
+        super.endDocument();
+
+        for (int i = 0; i < count; i++)
+        {
+            IDocumentRule rule = (IDocumentRule) _documentRules.get(i);
+
+            try
+            {
+                rule.finish();
+            }
+            catch (Exception ex)
+            {
+                throw createSAXException(ex);
+            }
+        }
+    }
+
+    public void startDocument() throws SAXException
+    {
+        _firstElement = true;
+
+        super.startDocument();
+    }
+
+    public void startElement(
+        String namespaceURI,
+        String localName,
+        String qName,
+        Attributes attributes)
+        throws SAXException
+    {
+        if (_firstElement)
+        {
+            sendStartDocumentNotification(namespaceURI, localName, qName, attributes);
+
+            _firstElement = false;
+        }
+
+        super.startElement(namespaceURI, localName, qName, attributes);
+    }
+
+    private void sendStartDocumentNotification(
+        String namespaceURI,
+        String localName,
+        String qName,
+        Attributes attributes)
+        throws SAXException
+    {
+        int count = Tapestry.size(_documentRules);
+
+        String name = Tapestry.isBlank(localName) ? qName : localName;
+
+        for (int i = 0; i < count; i++)
+        {
+            IDocumentRule rule = (IDocumentRule) _documentRules.get(i);
+
+            try
+            {
+                rule.startDocument(namespaceURI, name, attributes);
+            }
+            catch (Exception ex)
+            {
+                throw createSAXException(ex);
+            }
+        }
+
+    }
+
+    /**
+     * Invokes {@link #fatalError(SAXParseException)}.
+     */
+    public void error(SAXParseException exception) throws SAXException
+    {
+        fatalError(exception);
+    }
+
+    /**
+     * Simply re-throws the exception.  All exceptions when parsing
+     * documents are fatal.
+     */
+    public void fatalError(SAXParseException exception) throws SAXException
+    {
+        throw exception;
+    }
+
+    /**
+     * Invokes {@link #fatalError(SAXParseException)}.
+     */
+    public void warning(SAXParseException exception) throws SAXException
+    {
+        fatalError(exception);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/SpecificationParser.java b/tapestry-framework/src/org/apache/tapestry/parse/SpecificationParser.java
new file mode 100644
index 0000000..01c9318
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/SpecificationParser.java
@@ -0,0 +1,1295 @@
+//  Copyright 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.tapestry.parse;
+
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.digester.Rule;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ILocationHolder;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.spec.AssetType;
+import org.apache.tapestry.spec.BeanLifecycle;
+import org.apache.tapestry.spec.BindingType;
+import org.apache.tapestry.spec.Direction;
+import org.apache.tapestry.spec.IApplicationSpecification;
+import org.apache.tapestry.spec.IComponentSpecification;
+import org.apache.tapestry.spec.IExtensionSpecification;
+import org.apache.tapestry.spec.ILibrarySpecification;
+import org.apache.tapestry.spec.SpecFactory;
+import org.apache.tapestry.util.xml.DocumentParseException;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXParseException;
+
+/**
+ *  Used to parse an application or component specification into a
+ *  {@link org.apache.tapestry.spec.ApplicationSpecification} or {@link IComponentSpecification}.
+ *
+ *
+ *  <table border=1
+ *	<tr>
+ *	  <th>Version</th> <th>PUBLIC ID</th> <th>SYSTEM ID</th> <th>Description</th>
+ *  </tr>
+ *
+ * 
+ *  <tr valign="top">
+ *  <td>1.3</td>
+ *  <td><code>-//Howard Lewis Ship//Tapestry Specification 1.3//EN</code></td>
+ * <td><code>http://tapestry.sf.net/dtd/Tapestry_1_3.dtd</code></td>
+ *  <td>
+ *  Version of specification introduced in release 2.2.
+ * </td>
+ * </tr>
+ *
+ *  <tr valign="top">
+ *  <td>3.0</td>
+ *  <td><code>-//Howard Lewis Ship//Tapestry Specification 3.0//EN</code></td>
+ * <td><code>http://tapestry.sf.net/dtd/Tapestry_3_0.dtd</code></td>
+ *  <td>
+ *  Version of specification introduced in release 3.0.
+ *  <br/>
+ *  Note: Future DTD versions will track Tapestry release numbers.
+ * </td>
+ * </tr>
+ * 
+ * 
+ *  </table>
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ * 
+ **/
+
+public class SpecificationParser
+{
+    private static final Log LOG = LogFactory.getLog(SpecificationParser.class);
+
+    /** @since 2.2 **/
+
+    public static final String TAPESTRY_DTD_1_3_PUBLIC_ID =
+        "-//Howard Lewis Ship//Tapestry Specification 1.3//EN";
+
+    /** @since 3.0 **/
+
+    public static final String TAPESTRY_DTD_3_0_PUBLIC_ID =
+        "-//Apache Software Foundation//Tapestry Specification 3.0//EN";
+
+    /**
+     *  Like modified property name, but allows periods in the name as
+     *  well.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public static final String EXTENDED_PROPERTY_NAME_PATTERN = "^_?[a-zA-Z](\\w|-|\\.)*$";
+
+    /**
+     *  Perl5 pattern that parameter names must conform to.  
+     *  Letter, followed by letter, number or underscore.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public static final String PARAMETER_NAME_PATTERN = Tapestry.SIMPLE_PROPERTY_NAME_PATTERN;
+
+    /**
+     *  Perl5 pattern that property names (that can be connected to
+     *  parameters) must conform to.  
+     *  Letter, followed by letter, number or underscore.
+     *  
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public static final String PROPERTY_NAME_PATTERN = Tapestry.SIMPLE_PROPERTY_NAME_PATTERN;
+
+    /**
+     *  Perl5 pattern for page names.  Letter
+     *  followed by letter, number, dash, underscore or period.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public static final String PAGE_NAME_PATTERN = EXTENDED_PROPERTY_NAME_PATTERN;
+
+    /**
+     *  Perl5 pattern for component alias. 
+     *  Letter, followed by letter, number, or underscore.
+     *  This is used to validate component types registered
+     *  in the application or library specifications.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public static final String COMPONENT_ALIAS_PATTERN = Tapestry.SIMPLE_PROPERTY_NAME_PATTERN;
+
+    /**
+     *  Perl5 pattern for helper bean names.  
+     *  Letter, followed by letter, number or underscore.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public static final String BEAN_NAME_PATTERN = Tapestry.SIMPLE_PROPERTY_NAME_PATTERN;
+
+    /**
+     *  Perl5 pattern for component ids.  Letter, followed by
+     *  letter, number or underscore.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public static final String COMPONENT_ID_PATTERN = Tapestry.SIMPLE_PROPERTY_NAME_PATTERN;
+
+    /**
+     *  Perl5 pattern for asset names.  Letter, followed by
+     *  letter, number or underscore.  Also allows
+     *  the special "$template" value.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public static final String ASSET_NAME_PATTERN =
+        "(\\$template)|(" + Tapestry.SIMPLE_PROPERTY_NAME_PATTERN + ")";
+
+    /**
+     *  Perl5 pattern for service names.  Letter
+     *  followed by letter, number, dash, underscore or period.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public static final String SERVICE_NAME_PATTERN = EXTENDED_PROPERTY_NAME_PATTERN;
+
+    /**
+     *  Perl5 pattern for library ids.  Letter followed
+     *  by letter, number or underscore.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public static final String LIBRARY_ID_PATTERN = Tapestry.SIMPLE_PROPERTY_NAME_PATTERN;
+
+    /**
+     *  Per5 pattern for extension names.  Letter followed
+     *  by letter, number, dash, period or underscore. 
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public static final String EXTENSION_NAME_PATTERN = EXTENDED_PROPERTY_NAME_PATTERN;
+
+    /**
+     *  Perl5 pattern for component types.  Component types are an optional
+     *  namespace prefix followed by a normal identifier.
+     * 
+     *  @since 2.2
+     **/
+
+    public static final String COMPONENT_TYPE_PATTERN = "^(_?[a-zA-Z]\\w*:)?[a-zA-Z_](\\w)*$";
+
+    /**
+     *  We can share a single map for all the XML attribute to object conversions,
+     *  since the keys are unique.
+     * 
+     **/
+
+    private static final Map CONVERSION_MAP = new HashMap();
+
+    /** @since 1.0.9 **/
+
+    private SpecFactory _factory;
+
+    /** 
+     *   Digester used for component specifications.
+     * 
+     *  @since 3.0 
+     * 
+     **/
+
+    private SpecificationDigester _componentDigester;
+
+    /**
+     *  Digestger used for page specifications.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    private SpecificationDigester _pageDigester;
+
+    /**
+     *  Digester use for library specifications.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    private SpecificationDigester _libraryDigester;
+
+    /**
+     *  @since 3.0 
+     * 
+     **/
+
+    private IResourceResolver _resolver;
+
+    private interface IConverter
+    {
+        public Object convert(String value) throws DocumentParseException;
+    }
+
+    private static class BooleanConverter implements IConverter
+    
+    {
+        public Object convert(String value) throws DocumentParseException
+        {
+            Object result = CONVERSION_MAP.get(value.toLowerCase());
+
+            if (result == null || !(result instanceof Boolean))
+                throw new DocumentParseException(
+                    Tapestry.format("SpecificationParser.fail-convert-boolean", value));
+
+            return result;
+        }
+    }
+
+    private static class IntConverter implements IConverter
+    {
+        public Object convert(String value) throws DocumentParseException
+        {
+            try
+            {
+                return new Integer(value);
+            }
+            catch (NumberFormatException ex)
+            {
+                throw new DocumentParseException(
+                    Tapestry.format("SpecificationParser.fail-convert-int", value),
+                    ex);
+            }
+        }
+    }
+
+    private static class LongConverter implements IConverter
+    {
+        public Object convert(String value) throws DocumentParseException
+        {
+            try
+            {
+                return new Long(value);
+            }
+            catch (NumberFormatException ex)
+            {
+                throw new DocumentParseException(
+                    Tapestry.format("SpecificationParser.fail-convert-long", value),
+                    ex);
+            }
+        }
+    }
+
+    private static class DoubleConverter implements IConverter
+    {
+        public Object convert(String value) throws DocumentParseException
+        {
+            try
+            {
+                return new Double(value);
+            }
+            catch (NumberFormatException ex)
+            {
+                throw new DocumentParseException(
+                    Tapestry.format("SpecificationParser.fail-convert-double", value),
+                    ex);
+            }
+        }
+    }
+
+    private static class StringConverter implements IConverter
+    {
+        public Object convert(String value)
+        {
+            return value.trim();
+        }
+    }
+
+    /** 
+     *  Base class for creating locatable objects using the 
+     *  {@link SpecFactory}.
+     * 
+     **/
+
+    private abstract static class SpecFactoryCreateRule extends AbstractSpecificationRule
+    {
+        /**
+         *  Implement in subclass to create correct locatable object.
+         * 
+         **/
+
+        public abstract ILocationHolder create();
+
+        public void begin(String namespace, String name, Attributes attributes) throws Exception
+        {
+            ILocationHolder holder = create();
+
+            holder.setLocation(getLocation());
+
+            digester.push(holder);
+        }
+
+        public void end(String namespace, String name) throws Exception
+        {
+            digester.pop();
+        }
+
+    }
+
+    private class CreateExpressionBeanInitializerRule extends SpecFactoryCreateRule
+    {
+        public ILocationHolder create()
+        {
+            return _factory.createExpressionBeanInitializer();
+        }
+    }
+
+    private class CreateStringBeanInitializerRule extends SpecFactoryCreateRule
+    {
+        public ILocationHolder create()
+        {
+            return _factory.createMessageBeanInitializer();
+        }
+    }
+
+    private class CreateContainedComponentRule extends SpecFactoryCreateRule
+    {
+        public ILocationHolder create()
+        {
+            return _factory.createContainedComponent();
+        }
+    }
+
+    private class CreateParameterSpecificationRule extends SpecFactoryCreateRule
+    {
+        public ILocationHolder create()
+        {
+            return _factory.createParameterSpecification();
+        }
+    }
+
+    private class CreateComponentSpecificationRule extends SpecFactoryCreateRule
+    {
+        public ILocationHolder create()
+        {
+            return _factory.createComponentSpecification();
+        }
+    }
+
+    private class CreateBindingSpecificationRule extends SpecFactoryCreateRule
+    {
+        public ILocationHolder create()
+        {
+            return _factory.createBindingSpecification();
+        }
+    }
+
+    private class CreateBeanSpecificationRule extends SpecFactoryCreateRule
+    {
+        public ILocationHolder create()
+        {
+            return _factory.createBeanSpecification();
+        }
+    }
+
+    private class CreateListenerBindingSpecificationRule extends SpecFactoryCreateRule
+    {
+        public ILocationHolder create()
+        {
+            return _factory.createListenerBindingSpecification();
+        }
+    }
+
+    private class CreateAssetSpecificationRule extends SpecFactoryCreateRule
+    {
+        public ILocationHolder create()
+        {
+            return _factory.createAssetSpecification();
+        }
+    }
+
+    private class CreatePropertySpecificationRule extends SpecFactoryCreateRule
+    {
+        public ILocationHolder create()
+        {
+            return _factory.createPropertySpecification();
+        }
+    }
+
+    private class CreateApplicationSpecificationRule extends SpecFactoryCreateRule
+    {
+        public ILocationHolder create()
+        {
+            return _factory.createApplicationSpecification();
+        }
+    }
+
+    private class CreateLibrarySpecificationRule extends SpecFactoryCreateRule
+    {
+        public ILocationHolder create()
+        {
+            return _factory.createLibrarySpecification();
+        }
+    }
+
+    private class CreateExtensionSpecificationRule extends SpecFactoryCreateRule
+    {
+        public ILocationHolder create()
+        {
+            return _factory.createExtensionSpecification();
+        }
+    }
+
+    private static class ProcessExtensionConfigurationRule extends AbstractSpecificationRule
+    {
+        private String _value;
+        private String _propertyName;
+        private IConverter _converter;
+
+        public void begin(String namespace, String name, Attributes attributes) throws Exception
+        {
+            _propertyName = getValue(attributes, "property-name");
+            _value = getValue(attributes, "value");
+
+            String type = getValue(attributes, "type");
+
+            _converter = (IConverter) CONVERSION_MAP.get(type);
+
+            if (_converter == null)
+                throw new DocumentParseException(
+                    Tapestry.format("SpecificationParser.unknown-static-value-type", type),
+                    getResourceLocation());
+
+        }
+
+        public void body(String namespace, String name, String text) throws Exception
+        {
+            if (Tapestry.isBlank(text))
+                return;
+
+            if (_value != null)
+                throw new DocumentParseException(
+                    Tapestry.format("SpecificationParser.no-attribute-and-body", "value", name),
+                    getResourceLocation());
+
+            _value = text.trim();
+        }
+
+        public void end(String namespace, String name) throws Exception
+        {
+            if (_value == null)
+                throw new DocumentParseException(
+                    Tapestry.format(
+                        "SpecificationParser.required-extended-attribute",
+                        name,
+                        "value"),
+                    getResourceLocation());
+
+            Object objectValue = _converter.convert(_value);
+
+            IExtensionSpecification top = (IExtensionSpecification) digester.peek();
+
+            top.addConfiguration(_propertyName, objectValue);
+
+            _converter = null;
+            _value = null;
+            _propertyName = null;
+
+        }
+
+    }
+
+    // Identify all the different acceptible values.
+    // We continue to sneak by with a single map because
+    // there aren't conflicts;  when we have 'foo' meaning
+    // different things in different places in the DTD, we'll
+    // need multiple maps.
+
+    static {
+
+        CONVERSION_MAP.put("true", Boolean.TRUE);
+        CONVERSION_MAP.put("t", Boolean.TRUE);
+        CONVERSION_MAP.put("1", Boolean.TRUE);
+        CONVERSION_MAP.put("y", Boolean.TRUE);
+        CONVERSION_MAP.put("yes", Boolean.TRUE);
+        CONVERSION_MAP.put("on", Boolean.TRUE);
+
+        CONVERSION_MAP.put("false", Boolean.FALSE);
+        CONVERSION_MAP.put("f", Boolean.FALSE);
+        CONVERSION_MAP.put("0", Boolean.FALSE);
+        CONVERSION_MAP.put("off", Boolean.FALSE);
+        CONVERSION_MAP.put("no", Boolean.FALSE);
+        CONVERSION_MAP.put("n", Boolean.FALSE);
+
+        CONVERSION_MAP.put("none", BeanLifecycle.NONE);
+        CONVERSION_MAP.put("request", BeanLifecycle.REQUEST);
+        CONVERSION_MAP.put("page", BeanLifecycle.PAGE);
+        CONVERSION_MAP.put("render", BeanLifecycle.RENDER);
+
+        CONVERSION_MAP.put("boolean", new BooleanConverter());
+        CONVERSION_MAP.put("int", new IntConverter());
+        CONVERSION_MAP.put("double", new DoubleConverter());
+        CONVERSION_MAP.put("String", new StringConverter());
+        CONVERSION_MAP.put("long", new LongConverter());
+
+        CONVERSION_MAP.put("in", Direction.IN);
+        CONVERSION_MAP.put("form", Direction.FORM);
+        CONVERSION_MAP.put("custom", Direction.CUSTOM);
+        CONVERSION_MAP.put("auto", Direction.AUTO);
+    }
+
+    public SpecificationParser(IResourceResolver resolver)
+    {
+        _resolver = resolver;
+        setFactory(new SpecFactory());
+    }
+
+    /**
+     *  Parses an input stream containing a page or component specification and assembles
+     *  an {@link IComponentSpecification} from it.  
+     *
+     *  @throws DocumentParseException if the input stream cannot be fully
+     *  parsed or contains invalid data.
+     *
+     **/
+
+    public IComponentSpecification parseComponentSpecification(IResourceLocation resourceLocation)
+        throws DocumentParseException
+    {
+        if (_componentDigester == null)
+            _componentDigester = constructComponentDigester();
+
+        try
+        {
+            IComponentSpecification result =
+                (IComponentSpecification) parse(_componentDigester, resourceLocation);
+
+            result.setSpecificationLocation(resourceLocation);
+
+            return result;
+        }
+        catch (DocumentParseException ex)
+        {
+            _componentDigester = null;
+
+            throw ex;
+        }
+    }
+
+    /**
+     *  Parses a resource using a particular digester.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    protected Object parse(SpecificationDigester digester, IResourceLocation location)
+        throws DocumentParseException
+    {
+        try
+        {
+            if (LOG.isDebugEnabled())
+                LOG.debug("Parsing " + location);
+
+            URL url = location.getResourceURL();
+
+            if (url == null)
+                throw new DocumentParseException(
+                    Tapestry.format("AbstractDocumentParser.missing-resource", location),
+                    location);
+
+            InputSource source = new InputSource(url.toExternalForm());
+
+            digester.setResourceLocation(location);
+
+            Object result = digester.parse(source);
+
+            if (LOG.isDebugEnabled())
+                LOG.debug("Result: " + result);
+
+            return result;
+        }
+        catch (SAXParseException ex)
+        {
+            throw new DocumentParseException(ex);
+        }
+        catch (DocumentParseException ex)
+        {
+            throw ex;
+        }
+        catch (Exception ex)
+        {
+            throw new DocumentParseException(
+                Tapestry.format(
+                    "SpecificationParser.error-reading-resource",
+                    location,
+                    ex.getMessage()),
+                location,
+                ex);
+        }
+        finally
+        {
+            digester.setResourceLocation(null);
+        }
+    }
+
+    /**
+     *  Parses an input stream containing a page specification and assembles
+     *  an {@link IComponentSpecification} from it.  
+     *
+     *  @throws DocumentParseException if the input stream cannot be fully
+     *  parsed or contains invalid data.
+     * 
+     *  @since 2.2
+     *
+     **/
+
+    public IComponentSpecification parsePageSpecification(IResourceLocation resourceLocation)
+        throws DocumentParseException
+    {
+        if (_pageDigester == null)
+            _pageDigester = constructPageDigester();
+
+        try
+        {
+            IComponentSpecification result =
+                (IComponentSpecification) parse(_pageDigester, resourceLocation);
+
+            result.setSpecificationLocation(resourceLocation);
+
+            return result;
+        }
+        catch (DocumentParseException ex)
+        {
+            _pageDigester = null;
+
+            throw ex;
+        }
+    }
+
+    /**
+     *  Parses an resource containing an application specification and assembles
+     *  an {@link org.apache.tapestry.spec.ApplicationSpecification} from it.
+     *
+     *  @throws DocumentParseException if the input stream cannot be fully
+     *  parsed or contains invalid data.
+     *
+     **/
+
+    public IApplicationSpecification parseApplicationSpecification(IResourceLocation resourceLocation)
+        throws DocumentParseException
+    {
+
+        // Use a one-shot digester, because you only parse the app spec
+        // once.
+
+        IApplicationSpecification result =
+            (IApplicationSpecification) parse(constructApplicationDigester(), resourceLocation);
+
+        result.setResourceResolver(_resolver);
+        result.setSpecificationLocation(resourceLocation);
+        result.instantiateImmediateExtensions();
+
+        return result;
+    }
+
+    /**
+     *  Parses an input stream containing a library specification and assembles
+     *  a {@link org.apache.tapestry.spec.LibrarySpecification} from it.
+     *
+     *  @throws DocumentParseException if the input stream cannot be fully
+     *  parsed or contains invalid data.
+     * 
+     *  @since 2.2
+     *
+     **/
+
+    public ILibrarySpecification parseLibrarySpecification(IResourceLocation resourceLocation)
+        throws DocumentParseException
+    {
+        if (_libraryDigester == null)
+            _libraryDigester = constructLibraryDigester();
+
+        try
+        {
+            ILibrarySpecification result =
+                (ILibrarySpecification) parse(_libraryDigester, resourceLocation);
+
+            result.setResourceResolver(_resolver);
+            result.setSpecificationLocation(resourceLocation);
+            result.instantiateImmediateExtensions();
+
+            return result;
+        }
+        catch (DocumentParseException ex)
+        {
+            _libraryDigester = null;
+
+            throw ex;
+        }
+    }
+
+    /**
+     *  Sets the SpecFactory which instantiates Tapestry spec objects.
+     * 
+     *  @since 1.0.9
+     **/
+
+    public void setFactory(SpecFactory factory)
+    {
+        _factory = factory;
+    }
+
+    /**
+     *  Returns the current SpecFactory which instantiates Tapestry spec objects.
+     * 
+     *  @since 1.0.9
+     * 
+     **/
+
+    public SpecFactory getFactory()
+    {
+        return _factory;
+    }
+
+    /**
+     *  Constructs a digester, registerring the known DTDs and the
+     *  global rules (for &lt;property&gt; and &lt;description&gt;).
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    protected SpecificationDigester constructBaseDigester(String rootElement)
+    {
+        SpecificationDigester result = new SpecificationDigester();
+
+        // <description>
+
+        result.addBeanPropertySetter("*/description", "description");
+
+        // <property> 
+
+        result.addRule("*/property", new SetMetaPropertyRule());
+
+        result.register(TAPESTRY_DTD_1_3_PUBLIC_ID, getURL("Tapestry_1_3.dtd"));
+        result.register(TAPESTRY_DTD_3_0_PUBLIC_ID, getURL("Tapestry_3_0.dtd"));
+
+        result.addDocumentRule(
+            new ValidatePublicIdRule(
+                new String[] { TAPESTRY_DTD_1_3_PUBLIC_ID, TAPESTRY_DTD_3_0_PUBLIC_ID },
+                rootElement));
+
+        result.setValidating(true);
+
+        return result;
+
+    }
+
+    /**
+     *  Constructs a digester configued to parse application specifications.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    protected SpecificationDigester constructApplicationDigester()
+    {
+        SpecificationDigester result = constructBaseDigester("application");
+
+        String pattern = "application";
+
+        result.addRule(pattern, new CreateApplicationSpecificationRule());
+        result.addSetLimitedProperties(
+            pattern,
+            new String[] { "name", "engine-class" },
+            new String[] { "name", "engineClassName" });
+        result.addRule(pattern, new SetPublicIdRule());
+
+        configureLibraryCommon(result, "application");
+
+        return result;
+    }
+
+    /**
+     *  Constructs a digester configured to parse library specifications.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    protected SpecificationDigester constructLibraryDigester()
+    {
+        SpecificationDigester result = constructBaseDigester("library-specification");
+
+        String pattern = "library-specification";
+
+        result.addRule(pattern, new CreateLibrarySpecificationRule());
+        result.addRule(pattern, new SetPublicIdRule());
+
+        // Has no attributes
+
+        configureLibraryCommon(result, "library-specification");
+
+        return result;
+    }
+
+    /**
+     *  Configures a digester to parse the common elements of
+     *  a &lt;application&gt; or &lt;library-specification&gt;.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    protected void configureLibraryCommon(SpecificationDigester digester, String rootElementName)
+    {
+        String pattern = rootElementName + "/page";
+
+        // <page>
+
+        digester.addValidate(
+            pattern,
+            "name",
+            PAGE_NAME_PATTERN,
+            "SpecificationParser.invalid-page-name");
+        digester.addCallMethod(pattern, "setPageSpecificationPath", 2);
+        digester.addCallParam(pattern, 0, "name");
+        digester.addCallParam(pattern, 1, "specification-path");
+
+        // <component-type>
+
+        pattern = rootElementName + "/component-type";
+        digester.addValidate(
+            pattern,
+            "type",
+            COMPONENT_ALIAS_PATTERN,
+            "SpecificationParser.invalid-component-type");
+        digester.addCallMethod(pattern, "setComponentSpecificationPath", 2);
+        digester.addCallParam(pattern, 0, "type");
+        digester.addCallParam(pattern, 1, "specification-path");
+
+        // <component-alias>
+        // From 1.3 DTD, replaced with <component-type> in 3.0 DTD
+
+        pattern = rootElementName + "/component-alias";
+        digester.addValidate(
+            pattern,
+            "type",
+            COMPONENT_ALIAS_PATTERN,
+            "SpecificationParser.invalid-component-type");
+        digester.addCallMethod(pattern, "setComponentSpecificationPath", 2);
+        digester.addCallParam(pattern, 0, "type");
+        digester.addCallParam(pattern, 1, "specification-path");
+
+        // <service>
+
+        pattern = rootElementName + "/service";
+
+        digester.addValidate(
+            pattern,
+            "name",
+            SERVICE_NAME_PATTERN,
+            "SpecificationParser.invalid-service-name");
+        digester.addCallMethod(pattern, "setServiceClassName", 2);
+        digester.addCallParam(pattern, 0, "name");
+        digester.addCallParam(pattern, 1, "class");
+
+        // <library>
+
+        pattern = rootElementName + "/library";
+
+        digester.addValidate(
+            pattern,
+            "id",
+            LIBRARY_ID_PATTERN,
+            "SpecificationParser.invalid-library-id");
+        digester.addRule(pattern, new DisallowFrameworkNamespaceRule());
+        digester.addCallMethod(pattern, "setLibrarySpecificationPath", 2);
+        digester.addCallParam(pattern, 0, "id");
+        digester.addCallParam(pattern, 1, "specification-path");
+
+        // <extension>
+
+        pattern = rootElementName + "/extension";
+
+        digester.addRule(pattern, new CreateExtensionSpecificationRule());
+        digester.addValidate(
+            pattern,
+            "name",
+            EXTENSION_NAME_PATTERN,
+            "SpecificationParser.invalid-extension-name");
+        digester.addSetBooleanProperty(pattern, "immediate", "immediate");
+        digester.addSetLimitedProperties(pattern, "class", "className");
+        digester.addConnectChild(pattern, "addExtensionSpecification", "name");
+
+        // <configure> within <extension>
+
+        pattern = rootElementName + "/extension/configure";
+        digester.addValidate(
+            pattern,
+            "property-name",
+            PROPERTY_NAME_PATTERN,
+            "SpecificationParser.invalid-property-name");
+        digester.addRule(pattern, new ProcessExtensionConfigurationRule());
+
+    }
+
+    /**
+     *  Returns a digester configured to parse page specifications.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    protected SpecificationDigester constructPageDigester()
+    {
+        SpecificationDigester result = constructBaseDigester("page-specification");
+
+        // <page-specification>
+
+        String pattern = "page-specification";
+
+        result.addRule(pattern, new CreateComponentSpecificationRule());
+        result.addRule(pattern, new SetPublicIdRule());
+        result.addInitializeProperty(pattern, "pageSpecification", Boolean.TRUE);
+        result.addInitializeProperty(pattern, "allowBody", Boolean.TRUE);
+        result.addInitializeProperty(pattern, "allowInformalParameters", Boolean.FALSE);
+        result.addSetLimitedProperties(pattern, "class", "componentClassName");
+
+        configureCommon(result, "page-specification");
+
+        return result;
+    }
+
+    /**
+     *  Returns a digester configured to parse component specifications.
+     * 
+     **/
+
+    protected SpecificationDigester constructComponentDigester()
+    {
+        SpecificationDigester result = constructBaseDigester("component-specification");
+
+        // <component-specification>
+
+        String pattern = "component-specification";
+
+        result.addRule(pattern, new CreateComponentSpecificationRule());
+        result.addRule(pattern, new SetPublicIdRule());
+
+        result.addSetBooleanProperty(pattern, "allow-body", "allowBody");
+        result.addSetBooleanProperty(
+            pattern,
+            "allow-informal-parameters",
+            "allowInformalParameters");
+        result.addSetLimitedProperties(pattern, "class", "componentClassName");
+
+        // TODO: publicId
+
+        // <parameter>
+
+        pattern = "component-specification/parameter";
+
+        result.addRule(pattern, new CreateParameterSpecificationRule());
+        result.addValidate(
+            pattern,
+            "name",
+            PARAMETER_NAME_PATTERN,
+            "SpecificationParser.invalid-parameter-name");
+
+        result.addValidate(
+            pattern,
+            "property-name",
+            PROPERTY_NAME_PATTERN,
+            "SpecificationParser.invalid-property-name");
+
+        // We use a slight kludge to set the default propertyName from the 
+        // name attribute.  If the spec includes a property-name attribute, that
+        // will overwrite the default property name).
+        // Remember that digester rule order counts!
+
+        result.addSetLimitedProperties(pattern, "name", "propertyName");
+
+        // java-type is a holdover from the 1.3 DTD and will eventually be removed.
+
+        result.addSetLimitedProperties(
+            pattern,
+            new String[] { "property-name", "type", "java-type", "default-value" },
+            new String[] { "propertyName", "type", "type", "defaultValue" });
+
+        result.addSetBooleanProperty(pattern, "required", "required");
+
+        result.addSetConvertedProperty(pattern, CONVERSION_MAP, "direction", "direction");
+
+        result.addConnectChild(pattern, "addParameter", "name");
+
+        // <reserved-parameter>
+
+        pattern = "component-specification/reserved-parameter";
+
+        result.addCallMethod(pattern, "addReservedParameterName", 1);
+        result.addCallParam(pattern, 0, "name");
+
+        configureCommon(result, "component-specification");
+
+        return result;
+    }
+
+    /**
+     *  Configure the common elements shared by both &lt;page-specification&gt;
+     *  and &lt;component-specification&gt;.
+     * 
+     **/
+
+    protected void configureCommon(SpecificationDigester digester, String rootElementName)
+    {
+        // <bean>
+
+        String pattern = rootElementName + "/bean";
+
+        digester.addRule(pattern, new CreateBeanSpecificationRule());
+        digester.addValidate(
+            pattern,
+            "name",
+            BEAN_NAME_PATTERN,
+            "SpecificationParser.invalid-bean-name");
+        digester.addSetConvertedProperty(pattern, CONVERSION_MAP, "lifecycle", "lifecycle");
+        digester.addSetLimitedProperties(pattern, "class", "className");
+        digester.addConnectChild(pattern, "addBeanSpecification", "name");
+
+        // <set-property> inside <bean>
+
+        pattern = rootElementName + "/bean/set-property";
+
+        digester.addRule(pattern, new CreateExpressionBeanInitializerRule());
+        digester.addSetLimitedProperties(pattern, "name", "propertyName");
+        digester.addSetExtendedProperty(pattern, "expression", "expression", true);
+        digester.addSetNext(pattern, "addInitializer");
+
+        // <set-string-property> inside <bean>
+        // This is for compatibility with the 1.3 DTD
+
+        pattern = rootElementName + "/bean/set-string-property";
+
+        digester.addRule(pattern, new CreateStringBeanInitializerRule());
+        digester.addSetLimitedProperties(
+            pattern,
+            new String[] { "name", "key" },
+            new String[] { "propertyName", "key" });
+
+        digester.addSetNext(pattern, "addInitializer");
+
+        // It's now set-message-property in the 3.0 DTD
+
+        pattern = rootElementName + "/bean/set-message-property";
+
+        digester.addRule(pattern, new CreateStringBeanInitializerRule());
+        digester.addSetLimitedProperties(
+            pattern,
+            new String[] { "name", "key" },
+            new String[] { "propertyName", "key" });
+
+        digester.addSetNext(pattern, "addInitializer");
+
+        // <component>
+
+        pattern = rootElementName + "/component";
+
+        digester.addRule(pattern, new CreateContainedComponentRule());
+        digester.addValidate(
+            pattern,
+            "id",
+            COMPONENT_ID_PATTERN,
+            "SpecificationParser.invalid-component-id");
+        digester.addValidate(
+            pattern,
+            "type",
+            COMPONENT_TYPE_PATTERN,
+            "SpecificationParser.invalid-component-type");
+        digester.addSetLimitedProperties(pattern, "type", "type");
+        digester.addRule(pattern, new ComponentCopyOfRule());
+        digester.addConnectChild(pattern, "addComponent", "id");
+        digester.addSetBooleanProperty(
+            pattern,
+            "inherit-informal-parameters",
+            "inheritInformalParameters");
+
+        // <binding> inside <component>
+
+        pattern = rootElementName + "/component/binding";
+
+        Rule createBindingSpecificationRule = new CreateBindingSpecificationRule();
+
+        digester.addRule(pattern, createBindingSpecificationRule);
+        digester.addInitializeProperty(pattern, "type", BindingType.DYNAMIC);
+        digester.addSetExtendedProperty(pattern, "expression", "value", true);
+        digester.addConnectChild(pattern, "setBinding", "name");
+
+        // <field-binding> inside <component>
+        // For compatibility with 1.3 DTD only, removed in 3.0 DTD
+
+        pattern = rootElementName + "/component/field-binding";
+
+        digester.addRule(pattern, createBindingSpecificationRule);
+        digester.addInitializeProperty(pattern, "type", BindingType.FIELD);
+        digester.addSetExtendedProperty(pattern, "field-name", "value", true);
+        digester.addConnectChild(pattern, "setBinding", "name");
+
+        // <inherited-binding> inside <component>
+
+        pattern = rootElementName + "/component/inherited-binding";
+
+        digester.addRule(pattern, createBindingSpecificationRule);
+        digester.addInitializeProperty(pattern, "type", BindingType.INHERITED);
+        digester.addSetLimitedProperties(pattern, "parameter-name", "value");
+        digester.addConnectChild(pattern, "setBinding", "name");
+
+        // <static-binding> inside <component>
+
+        pattern = rootElementName + "/component/static-binding";
+
+        digester.addRule(pattern, createBindingSpecificationRule);
+        digester.addInitializeProperty(pattern, "type", BindingType.STATIC);
+        digester.addSetExtendedProperty(pattern, "value", "value", true);
+        digester.addConnectChild(pattern, "setBinding", "name");
+
+        // <string-binding> inside <component>
+        // Maintained just for 1.3 DTD compatibility
+
+        pattern = rootElementName + "/component/string-binding";
+
+        digester.addRule(pattern, createBindingSpecificationRule);
+        digester.addInitializeProperty(pattern, "type", BindingType.STRING);
+        digester.addSetLimitedProperties(pattern, "key", "value");
+        digester.addConnectChild(pattern, "setBinding", "name");
+
+        // Renamed to <message-binding> in the 3.0 DTD
+
+        pattern = rootElementName + "/component/message-binding";
+
+        digester.addRule(pattern, createBindingSpecificationRule);
+        digester.addInitializeProperty(pattern, "type", BindingType.STRING);
+        digester.addSetLimitedProperties(pattern, "key", "value");
+        digester.addConnectChild(pattern, "setBinding", "name");
+
+        // <listener-binding> inside <component>
+
+        pattern = rootElementName + "/component/listener-binding";
+
+        digester.addRule(pattern, new CreateListenerBindingSpecificationRule());
+        digester.addSetLimitedProperties(pattern, "language", "language");
+        digester.addBody(pattern, "value");
+        digester.addConnectChild(pattern, "setBinding", "name");
+
+        // <external-asset>
+
+        pattern = rootElementName + "/external-asset";
+
+        Rule createAssetSpecificationRule = new CreateAssetSpecificationRule();
+
+        digester.addRule(pattern, createAssetSpecificationRule);
+        digester.addInitializeProperty(pattern, "type", AssetType.EXTERNAL);
+        digester.addValidate(
+            pattern,
+            "name",
+            ASSET_NAME_PATTERN,
+            "SpecificationParser.invalid-asset-name");
+        digester.addSetLimitedProperties(pattern, "URL", "path");
+        digester.addConnectChild(pattern, "addAsset", "name");
+
+        // <context-asset>
+
+        pattern = rootElementName + "/context-asset";
+
+        digester.addRule(pattern, createAssetSpecificationRule);
+        digester.addInitializeProperty(pattern, "type", AssetType.CONTEXT);
+        digester.addValidate(
+            pattern,
+            "name",
+            ASSET_NAME_PATTERN,
+            "SpecificationParser.invalid-asset-name");
+
+        // TODO: $template$
+
+        digester.addSetLimitedProperties(pattern, "path", "path");
+        digester.addConnectChild(pattern, "addAsset", "name");
+
+        // <private-asset>
+
+        pattern = rootElementName + "/private-asset";
+
+        digester.addRule(pattern, createAssetSpecificationRule);
+        digester.addInitializeProperty(pattern, "type", AssetType.PRIVATE);
+        digester.addValidate(
+            pattern,
+            "name",
+            ASSET_NAME_PATTERN,
+            "SpecificationParser.invalid-asset-name");
+
+        // TODO: $template$
+
+        digester.addSetLimitedProperties(pattern, "resource-path", "path");
+        digester.addConnectChild(pattern, "addAsset", "name");
+
+        // <property-specification>
+
+        pattern = rootElementName + "/property-specification";
+
+        digester.addRule(pattern, new CreatePropertySpecificationRule());
+        digester.addValidate(
+            pattern,
+            "name",
+            PROPERTY_NAME_PATTERN,
+            "SpecificationParser.invalid-property-name");
+        digester.addSetLimitedProperties(
+            pattern,
+            new String[] { "name", "type" },
+            new String[] { "name", "type" });
+        digester.addSetBooleanProperty(pattern, "persistent", "persistent");
+        digester.addSetExtendedProperty(pattern, "initial-value", "initialValue", false);
+        digester.addSetNext(pattern, "addPropertySpecification");
+    }
+
+    private String getURL(String resource)
+    {
+        return getClass().getResource(resource).toExternalForm();
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/Tapestry_1_3.dtd b/tapestry-framework/src/org/apache/tapestry/parse/Tapestry_1_3.dtd
new file mode 100644
index 0000000..2a2fd14
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/Tapestry_1_3.dtd
@@ -0,0 +1,549 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<!-- $Id: Tapestry_1_3.dtd,v 1.18 2002/09/19 17:25:28 hship Exp $ -->

+<!--

+

+The DTD for Tapestry application, page and component specifications.

+Associated with the public identifier:

+

+	-//Howard Lewis Ship//Tapestry Specification 1.3//EN

+	

+The canonical location for the DTD is:

+

+	http://tapestry.sf.net/dtd/Tapestry_1_3.dtd

+

+For application specifications, the root element is application.

+

+For component specifications, the root element is component-specification.

+

+For page specifications, the root element is page-specification.

+

+For library specifiations, the root element is library-specification.

+

+This DTD is backwards compatible with the 1.2 DTD, with the following exceptions:

+- specification (in 1.1) has been split into page-specification and component-specification

+- added string-value element

+- added library-specification root element

+- added library element

+- added extension element

+- added static value type 'long'

+- allow <property> within <bean>, <component>, <extension>, <private-asset>, <context-asset> and <external-asset>

+- add "render" to bean lifecycle value

+- rename <binding>'s property-path attribute to expression

+- simplify set-property to use OGNL expressions

+- add "form" as parameter direction

+-->

+

+<!-- =======================================================

+Entity: attribute-flag

+

+For entity attributesthat take a boolean value, defines 'yes' and 'no'.

+The default varies, so isn't included here.

+-->

+<!ENTITY % attribute-flag "(yes|no)">

+

+

+<!-- =======================================================

+Entity: static-value-type

+

+For entity attributes that take a string but convert it to a real

+type.  Defaults to String.

+

+-->

+<!ENTITY % static-value-type "(boolean|int|long|double|String) 'String'">

+

+<!ENTITY % library-content "(description*, property*, (page|component-alias|service|library|extension)*)">

+

+<!-- =======================================================

+Element: application

+Root element

+

+Defines a Tapestry application.

+

+Attributes:

+  name: A textual name used to uniquely identify the application.

+  engine-class:  The Java class to instantiate as the application engine.

+-->

+<!ELEMENT application %library-content;>

+<!ATTLIST application

+  name CDATA #REQUIRED

+  engine-class CDATA #REQUIRED

+>

+

+<!-- =======================================================

+Element: bean

+Appears in: component-specification, page-specification

+

+Defines a JavaBean that will be used in some way by the component.  Beans

+are accessible via the components' beans property (which contains a property

+for each bean).  Beans are created as needed, and are discarded based on

+the lifecycle attribute.  Beans are typically used to extend the

+implementation of a component via aggregation.

+

+Attributes:

+  name: the name of the bean

+  class: the Java class to instantiate

+  lifecycle: when the reference to the bean should be discard

+  	"none" no lifecycle, the bean is created and returned, but not stored

+  	"request" the bean is retained until the end of the request cycle

+  	"page" the bean is retained for the lifespan of the page

+  	"render" the bean is retained until the end of the current page render

+  	

+Nothing prevents a bean for storing state; however, such state will

+not be associated with any particular session (unlike persistant page

+properties).  Further, because of page pooling, subsequent requests

+from the same client may be serviced by different instances of a page and

+(therefore) different bean instances.

+

+Beans that have the "request" lifecycle may be stored into a pool

+for later re-use (in the same or different page).

+

+The bean may have its properties set.  Properties are set on both

+newly instantiated beans, and beans that are retrieved from a pool.

+

+-->

+<!ELEMENT bean (description*, property*, (set-property | set-string-property)*)>

+<!ATTLIST bean

+  name CDATA #REQUIRED

+  class CDATA #REQUIRED

+  lifecycle (none|request|page|render) "request"

+>

+

+<!-- =======================================================

+Element: binding

+Appears in: component

+

+Binds a parameter of the component to a property of its container.

+

+Attributes:

+  name: The name of the component parameter to bind.

+  expression: The OGNL expression.

+-->

+<!ELEMENT binding EMPTY>

+<!ATTLIST binding

+  name CDATA #REQUIRED

+  expression CDATA #REQUIRED

+>

+

+<!-- =======================================================

+Element: configure

+Appears in: extension

+

+Configures one JavaBean property of an extension.

+

+Attributes:

+  property-name:  The name of the property to configure.

+  type: Conversion of property value.

+  

+ 

+-->

+

+<!ELEMENT configure (#PCDATA)>

+<!ATTLIST configure 

+  property-name CDATA #REQUIRED

+  type %static-value-type;

+>

+  

+<!-- =======================================================

+Element: component

+Contained by: component-specification, page-specification

+

+Defines a component contained by the component being specified.

+

+Attribute:

+  id: A unique identifier for the component within the container.

+  type: The type of component, either a well known logical name, or the complete path

+    to the component's specification.

+  copy-of: The id of a previously defined component; this component will be a copy

+    of the other component.

+

+The Tapestry page loader ensures that either type or copy-of is specified, but not both.

+-->

+<!ELEMENT component (property*, (binding | field-binding | inherited-binding | static-binding | string-binding)*)>

+<!ATTLIST component

+  id ID #REQUIRED

+  type CDATA #IMPLIED

+  copy-of IDREF #IMPLIED

+>

+

+<!-- =======================================================

+Element: component-alias

+Contained by: application

+

+Establishes a short logic name for a particular component that is used

+within the application.

+

+Attributes:

+  type: The logical name for the component.

+  specification-path:  The complete path to the component's specification.

+-->

+<!ELEMENT component-alias EMPTY>

+<!ATTLIST component-alias

+  type CDATA #REQUIRED

+  specification-path CDATA #REQUIRED

+>

+

+<!-- =======================================================

+Element: component-specification

+Root element

+

+A component specification.  It's attributes define the Java class to

+instantiate, whether the component may wrap other elements, and whether

+informal (undeclared) parameters are allowed.  Very similar to a page-specification,

+except that component-specification allows for parameters (formal and informal).

+

+Attributes:

+  class: The Java class to instantiate for the component.

+  allow-body:  If yes (the default), the component may wrap other elements (have a body).

+  allow-informal-parameters:  If yes (the default), informal parameters (parameters that are not

+    explictily defined) are allowed.

+-->

+<!ELEMENT component-specification 

+	(description*, parameter*, reserved-parameter*, property*,

+	(bean |	component | external-asset | context-asset | private-asset)*)>

+<!ATTLIST component-specification

+  class CDATA #REQUIRED

+  allow-body %attribute-flag; "yes"

+  allow-informal-parameters %attribute-flag; "yes"

+>

+

+<!-- =======================================================

+Element: context-asset

+Contained by: component-specification, page-specification

+

+An asset located in the same web application context as the running

+application.

+

+Attributes:

+  name: The name of the asset.

+  path: The path, relative to the web application context, of the resource.

+-->

+<!ELEMENT context-asset (property*)>

+<!ATTLIST context-asset

+  name CDATA #REQUIRED

+  path CDATA #REQUIRED

+>

+

+<!-- =======================================================

+Element: description

+Appears in: many

+

+Several elements may contain descriptions; these descriptions are

+optional.  The eventual goal is to provide help in some form of IDE.

+Currently, descriptions are optional and ignored.

+

+Attributes:

+  xml:lang the language that the description is expressed in.

+-->

+<!ELEMENT description (#PCDATA)>

+<!ATTLIST description

+  xml:lang NMTOKEN "en"

+>

+

+<!-- =======================================================

+Element: extension

+Contained by: application, library-specification

+

+Defines an extension, an object that is instantiated and configured

+(like a helper bean) and is then accessible, by name, from the

+containing library (or application).

+

+Attributes:

+  name: Name of the extension.

+  class: Java class to instantiate.

+  immediate: If true, the extension is instantiated early instead of as-needed.

+  

+-->

+<!ELEMENT extension (property*, configure*)>

+<!ATTLIST extension

+  name CDATA #REQUIRED

+  class CDATA #REQUIRED

+  immediate %attribute-flag; "no"

+>

+

+<!-- =======================================================

+Element: external-asset

+Contained by: component-specification, page-specification

+

+Defines an asset at some external source.

+

+Attributes:

+  name: The name of the asset.

+  URL: The URL used to reference the asset.

+-->

+<!ELEMENT external-asset (property*)>

+<!ATTLIST external-asset

+  name CDATA #REQUIRED

+  URL CDATA #REQUIRED

+>

+

+<!-- =======================================================

+Element: field-binding

+Appears in: component

+

+Binds a parameter of the component to a public static field of

+some Java object.

+

+Attributes:

+  name: The name of the component parameter to bind.

+  field-name:  The name of the field, of the form [package.]class.field.

+    The package may be ommitted if it is "java.lang".

+-->

+<!ELEMENT field-binding EMPTY>

+<!ATTLIST field-binding

+  name CDATA #REQUIRED

+  field-name CDATA #REQUIRED

+>

+

+<!-- =======================================================

+Element: inherited-binding

+Appears in: component

+

+Binds a parameter of the component to a parameter of the container.

+

+Attributes:

+  name: The name of the component parameter to bind.

+  parameter-name: The name of the container parameter to bind the

+    component parameter to.

+-->

+<!ELEMENT inherited-binding EMPTY>

+<!ATTLIST inherited-binding

+  name CDATA #REQUIRED

+  parameter-name CDATA #REQUIRED

+>

+

+<!-- =======================================================

+Element: library

+Appears in: application-specification, library-specification

+

+Defines a library used in the construction of the container

+(either another library, or the application itself).

+

+Attributes:

+  id: An identifier used to reference pages and components

+    provided by the library.

+  specification-path: The path to the resource that provides

+    the library specification.

+-->

+<!ELEMENT library EMPTY>

+<!ATTLIST library

+  id CDATA #REQUIRED

+  specification-path CDATA #REQUIRED

+>

+

+<!-- =======================================================

+Element: library-specification

+Root element

+

+Defines a library that may be used in the construction 

+of an application (or another library).  An application can

+be thought of as a specialized kind of library.

+

+-->

+<!ELEMENT library-specification %library-content;>

+

+<!-- =======================================================

+Element: page

+Contained by: application, library-specification

+

+Defines a single page within the application.  Each application will contain

+at least one of these, to define the Home page.

+

+Attributes:

+  name: A unique name for the application.

+  specification-path:  The resource classpath of the component specification

+    for the page.

+-->

+<!ELEMENT page EMPTY>

+<!ATTLIST page

+  name CDATA #REQUIRED

+  specification-path CDATA #REQUIRED

+>

+

+<!-- =======================================================

+Element: page-specification

+Root element

+

+A page specification.  It's attributes define the Java class to

+instantiate.  Pages are like components, except they always allow

+a body, and never allow parameters (formal or otherwise).

+

+Attributes:

+  class: The Java class to instantiate for the component.

+-->

+<!ELEMENT page-specification (description*, property*,

+    (bean | component | external-asset | context-asset | private-asset)*)>

+<!ATTLIST page-specification

+  class CDATA #REQUIRED

+>

+

+<!-- =======================================================

+Element: parameter

+Contained by: component-specification

+

+Defines a formal parameter for the component.

+

+Attributes:

+  name: A unqiue name for the parameter.

+  java-type: The name of a Java class or primitive type expected by the parameter.

+    This is for documentation purposes only, it is not enforced.

+  required: If yes, then the parameter must be bound.  If no (the default),

+    then the parameter is optional.

+  property-name: The name to use, instead of the parameter name, for the

+    JavaBean property connected to this parameter.

+  direction: The normal flow of data through the component.

+-->

+

+<!ELEMENT parameter (description*)>

+<!ATTLIST parameter

+  name CDATA #REQUIRED

+  java-type CDATA #IMPLIED

+  required %attribute-flag; "no"

+  property-name CDATA #IMPLIED

+  direction (in|form|custom) "custom"

+>

+

+<!-- =======================================================

+Element: private-asset

+Contained by: component-specification, page-specification

+

+An asset available within the Java classpath (i.e., bundled inside a JAR or WAR).

+

+Attributes:

+  name: The name of the asset.

+  resource-path: The complete pathname of the resource.

+-->

+<!ELEMENT private-asset (property*)>

+<!ATTLIST private-asset

+  name CDATA #REQUIRED

+  resource-path CDATA #REQUIRED

+>

+

+<!-- =======================================================

+Element: property

+Contained by: (many other elements)

+

+Defines a key/value pair associated with the application or component specification.  Properties

+are used to capture information that doesn't fit into the DTD.  The value for the property is

+the PCDATA wrapped by the property tag (which is trimmed of leading and trailing whitespace).

+

+This should not be confused with several other tags which are used to set JavaBeans properties

+of various objects.  The <property> tag exists to allow meta-data to be stored in the specification.

+

+Attributes:

+  name: The name of the property to set.

+  

+-->

+<!ELEMENT property (#PCDATA)>

+<!ATTLIST property

+  name CDATA #REQUIRED

+>

+

+<!-- =======================================================

+Element: reserved-parameter

+Appears in: component-specification

+

+Identifies a name which may not be used as an informal parameter.

+Informal parameters are typically HTML attribute names; this

+list identifies HTML attributes that are written exclusively

+by the component and may not be affected by informal parameters.

+

+Attributes:

+  name: The parameter name to reserve.

+-->

+

+<!ELEMENT reserved-parameter EMPTY>

+<!ATTLIST reserved-parameter

+  name CDATA #REQUIRED

+>

+

+<!-- =======================================================

+Element: service

+Appears in: application

+

+Defines an engine service.  You may override the default

+set of engine services or provide completely new services.

+

+Attributes:

+  name: The name of the service.

+  class: The Java class to instantiate for the service.

+

+-->

+

+<!ELEMENT service EMPTY>

+<!ATTLIST service

+  name CDATA #REQUIRED

+  class CDATA #REQUIRED

+>

+

+<!-- =======================================================

+Element: set-property

+Appears in: bean

+

+Used to initialize a property of a helper bean.

+

+Attributes:

+  name: The name of the property to be set.

+  expression: The OGNL expression that provides a value.

+-->

+

+<!ELEMENT set-property EMPTY>

+<!ATTLIST set-property

+  name CDATA #REQUIRED

+  expression CDATA #REQUIRED

+>

+

+

+<!-- =======================================================

+Element: set-string-property

+Appears in: bean

+

+A localized string.

+

+Attributes:

+  key:  Sets a property of a string from a localized string.

+

+-->

+

+<!ELEMENT set-string-property EMPTY>

+<!ATTLIST set-string-property

+  name CDATA #REQUIRED

+  key CDATA #REQUIRED

+>

+

+

+<!-- =======================================================

+Element: static-binding

+Appears in: component

+

+Binds a parameter of the component to a static value defined directly

+within this specification. The value is the PCDATA wrapped by the element

+(with leading and trailing whitespace removed).

+

+Attributes:

+  name: The name of the component parameter to bind.

+

+-->

+<!ELEMENT static-binding (#PCDATA)>

+<!ATTLIST static-binding

+  name CDATA #REQUIRED

+>

+

+<!-- =======================================================

+Element: string-binding

+Appears in: component

+

+Binds a parameter of the component to a localized string of

+its container.

+

+Attributes:

+  name: The name of the component parameter to bind.

+  key: The key used to access a localized string.

+  

+-->

+

+<!ELEMENT string-binding EMPTY>

+<!ATTLIST string-binding

+  name CDATA #REQUIRED

+  key CDATA #REQUIRED

+>

+

diff --git a/tapestry-framework/src/org/apache/tapestry/parse/Tapestry_3_0.dtd b/tapestry-framework/src/org/apache/tapestry/parse/Tapestry_3_0.dtd
new file mode 100644
index 0000000..3036a5b
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/Tapestry_3_0.dtd
@@ -0,0 +1,547 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<!-- $Id: Tapestry_1_3.dtd,v 1.18 2002/09/19 17:25:28 hship Exp $ -->

+<!--

+

+The DTD for Tapestry application, library, page and component specifications.

+Associated with the public identifier:

+

+	-//Apache Software Foundation//Tapestry Specification 3.0//EN

+	

+The canonical location for the DTD is:

+

+	http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd

+

+For application specifications, the root element is application.

+

+For component specifications, the root element is component-specification.

+

+For page specifications, the root element is page-specification.

+

+For library specifiations, the root element is library-specification.

+

+This DTD is backwards compatible with the 1.3 DTD, with the following exceptions:

+- <field-binding> has been removed

+- attribute class of <component-specification> and <page-specification is now optional

+- attributes name and engine-class of <application> are now optional

+- added value attribute to <static-binding>

+- added value attribute to <property>

+- renamed <component-alias> to <component-type>

+- rename <set-string-property> to <set-message-property>

+- added <listener-binding> element

+- added <property-specification> element

+- renamed java-type to type inside <parameter>

+- allow values to be specified as attributes or wrapped character data in many elements

+- allow only a single <description> per element

+-->

+<!-- =======================================================

+Entity: attribute-flag

+

+For entity attributes that take a boolean value, defines 'yes' and 'no'.

+The default varies, so isn't included here.

+-->

+<!ENTITY % attribute-flag "(yes|no)">

+<!-- =======================================================

+Entity: static-value-type

+

+For entity attributes that take a string but convert it to a real

+type.  Defaults to String.

+

+-->

+<!ENTITY % static-value-type "(boolean|int|long|double|String) 'String'">

+<!ENTITY % library-content "(description?, property*, (page|component-type|service|library|extension)*)">

+<!-- =======================================================

+Element: application

+Root element

+

+Defines a Tapestry application.

+

+Attributes:

+  name: A textual name used to uniquely identify the application.

+  engine-class:  The Java class to instantiate as the application engine.

+-->

+<!ELEMENT application %library-content;>

+<!ATTLIST application

+	name CDATA #IMPLIED

+	engine-class CDATA #IMPLIED

+>

+<!-- =======================================================

+Element: bean

+Appears in: component-specification, page-specification

+

+Defines a JavaBean that will be used in some way by the component.  Beans

+are accessible via the components' beans property (which contains a property

+for each bean).  Beans are created as needed, and are discarded based on

+the lifecycle attribute.  Beans are typically used to extend the

+implementation of a component via aggregation.

+

+Attributes:

+  name: the name of the bean

+  class: the Java class to instantiate

+  lifecycle: when the reference to the bean should be discard

+  	"none" no lifecycle, the bean is created and returned, but not stored

+  	"request" the bean is retained until the end of the request cycle

+  	"page" the bean is retained for the lifespan of the page

+  	"render" the bean is retained until the end of the current page render

+  	

+Nothing prevents a bean for storing state; however, such state will

+not be associated with any particular session (unlike persistant page

+properties).  Further, because of page pooling, subsequent requests

+from the same client may be serviced by different instances of a page and

+(therefore) different bean instances.

+

+Beans that have the "request" lifecycle may be stored into a pool

+for later re-use (in the same or different page).

+

+The bean may have its properties set.  Properties are set on both

+newly instantiated beans, and beans that are retrieved from a pool.

+

+-->

+<!ELEMENT bean (description?, property*, (set-property | set-message-property)*)>

+<!ATTLIST bean

+	name CDATA #REQUIRED

+	class CDATA #REQUIRED

+	lifecycle (none | request | page | render) "request"

+>

+<!-- =======================================================

+Element: binding

+Appears in: component

+

+Binds a parameter of the component to a OGNL expression, relative

+to its container.  The expression may be provided as an attribute, or 

+as the body of the element.  The latter case is useful when the 

+expression is long, or uses problematic characters (such as a 

+mix of single and double quotes).

+

+Attributes:

+  name: The name of the component parameter to bind.

+  expression: The OGNL expression.

+-->

+<!ELEMENT binding (#PCDATA)>

+<!ATTLIST binding

+	name CDATA #REQUIRED

+	expression CDATA #IMPLIED

+>

+<!-- =======================================================

+Element: configure

+Appears in: extension

+

+Configures one JavaBean property of an extension.

+

+Attributes:

+  property-name:  The name of the property to configure.

+  type: Conversion of property value.

+  value: The value to be converted and applied.  If not 

+    specified, the element's character data is

+    used.  

+ 

+-->

+<!ELEMENT configure (#PCDATA)>

+<!ATTLIST configure

+	property-name CDATA #REQUIRED

+	type %static-value-type;

+	value CDATA #IMPLIED

+>

+<!-- =======================================================

+Element: component

+Contained by: component-specification, page-specification

+

+Defines a component contained by the component being specified.

+

+Attribute:

+  id: A unique identifier for the component within the container.

+  type: The type of component, either a well known logical name, or the complete path

+    to the component's specification.

+  copy-of: The id of a previously defined component; this component will be a copy

+    of the other component.

+

+The Tapestry page loader ensures that either type or copy-of is specified, but not both.

+-->

+<!ELEMENT component (property*, (binding | inherited-binding | listener-binding | static-binding | message-binding)*)>

+<!ATTLIST component

+	id ID #REQUIRED

+	type CDATA #IMPLIED

+	copy-of IDREF #IMPLIED

+	inherit-informal-parameters %attribute-flag; "no"

+>

+<!-- =======================================================

+Element: component-type

+Contained by: application

+

+Establishes a short logic name for a particular component that is used

+within the application.

+

+Attributes:

+  type: The logical name for the component.

+  specification-path:  The complete path to the component's specification.

+-->

+<!ELEMENT component-type EMPTY>

+<!ATTLIST component-type

+	type CDATA #REQUIRED

+	specification-path CDATA #REQUIRED

+>

+<!-- =======================================================

+Element: component-specification

+Root element

+

+A component specification.  It's attributes define the Java class to

+instantiate, whether the component may wrap other elements, and whether

+informal (undeclared) parameters are allowed.  Very similar to a page-specification,

+except that component-specification allows for parameters (formal and informal).

+

+Attributes:

+  class: The Java class to instantiate for the component.

+  allow-body:  If yes (the default), the component may wrap other elements (have a body).

+  allow-informal-parameters:  If yes (the default), informal parameters (parameters that are not

+    explictily defined) are allowed.

+-->

+<!ELEMENT component-specification (description?, parameter*, reserved-parameter*, property*, (bean | component | external-asset | context-asset | private-asset | property-specification)*)>

+<!ATTLIST component-specification

+	class CDATA #IMPLIED

+	allow-body %attribute-flag; "yes"

+	allow-informal-parameters %attribute-flag; "yes"

+>

+<!-- =======================================================

+Element: context-asset

+Contained by: component-specification, page-specification

+

+An asset located in the same web application context as the running

+application.

+

+Attributes:

+  name: The name of the asset.

+  path: The path, relative to the web application context, of the resource.

+-->

+<!ELEMENT context-asset (property*)>

+<!ATTLIST context-asset

+	name CDATA #REQUIRED

+	path CDATA #REQUIRED

+>

+<!-- =======================================================

+Element: description

+Appears in: many

+

+Several elements may contain descriptions; these descriptions are

+optional.  The eventual goal is to provide help in some form of IDE.

+Currently, descriptions are optional and ignored.

+

+-->

+<!ELEMENT description (#PCDATA)>

+<!-- =======================================================

+Element: extension

+Contained by: application, library-specification

+

+Defines an extension, an object that is instantiated and configured

+(like a helper bean) and is then accessible, by name, from the

+containing library (or application).

+

+Attributes:

+  name: Name of the extension.

+  class: Java class to instantiate.

+  immediate: If true, the extension is instantiated early instead of as-needed.

+  

+-->

+<!ELEMENT extension (property*, configure*)>

+<!ATTLIST extension

+	name CDATA #REQUIRED

+	class CDATA #REQUIRED

+	immediate %attribute-flag; "no"

+>

+<!-- =======================================================

+Element: external-asset

+Contained by: component-specification, page-specification

+

+Defines an asset at some external source.

+

+Attributes:

+  name: The name of the asset.

+  URL: The URL used to reference the asset.

+-->

+<!ELEMENT external-asset (property*)>

+<!ATTLIST external-asset

+	name CDATA #REQUIRED

+	URL CDATA #REQUIRED

+>

+<!-- =======================================================

+Element: inherited-binding

+Appears in: component

+

+Binds a parameter of the component to a parameter of the container.

+

+Attributes:

+  name: The name of the component parameter to bind.

+  parameter-name: The name of the container parameter to bind the

+    component parameter to.

+-->

+<!ELEMENT inherited-binding EMPTY>

+<!ATTLIST inherited-binding

+	name CDATA #REQUIRED

+	parameter-name CDATA #REQUIRED

+>

+<!-- =======================================================

+Element: library

+Appears in: application-specification, library-specification

+

+Defines a library used in the construction of the container

+(either another library, or the application itself).

+

+Attributes:

+  id: An identifier used to reference pages and components

+    provided by the library.

+  specification-path: The path to the resource that provides

+    the library specification.

+-->

+<!ELEMENT library EMPTY>

+<!ATTLIST library

+	id CDATA #REQUIRED

+	specification-path CDATA #REQUIRED

+>

+<!-- =======================================================

+Element: library-specification

+Root element

+

+Defines a library that may be used in the construction 

+of an application (or another library).  An application can

+be thought of as a specialized kind of library.

+

+-->

+<!ELEMENT library-specification %library-content;>

+<!-- =======================================================

+Element: page

+Contained by: application, library-specification

+

+Defines a single page within the application.  Each application will contain

+at least one of these, to define the Home page.

+

+Attributes:

+  name: A unique name for the application.

+  specification-path:  The resource classpath of the component specification

+    for the page.

+-->

+<!ELEMENT page EMPTY>

+<!ATTLIST page

+	name CDATA #REQUIRED

+	specification-path CDATA #REQUIRED

+>

+<!-- =======================================================

+Element: listener-binding

+Appears in: component

+

+Defines an in-place script using the scripting language

+supported by Bean Scripting Framework (http://jakarta.apache.org/bsf).

+The script itself is the element's character data (often, inside

+a CDATA block).

+

+The default language is jython, though this can be overridden.

+

+Attributes:

+  name: The name of the component parameter to bind.

+  language: The language the script is written in.

+-->

+<!ELEMENT listener-binding (#PCDATA)>

+<!ATTLIST listener-binding

+	name CDATA #REQUIRED

+	language CDATA #IMPLIED

+>

+<!-- =======================================================

+Element: page-specification

+Root element

+

+A page specification.  It's attributes define the Java class to

+instantiate.  Pages are like components, except they always allow

+a body, and never allow parameters (formal or otherwise).

+

+Attributes:

+  class: The Java class to instantiate for the component.

+-->

+<!ELEMENT page-specification (description?, property*, (bean | component | external-asset | context-asset | private-asset | property-specification)*)>

+<!ATTLIST page-specification

+	class CDATA #IMPLIED

+>

+<!-- =======================================================

+Element: parameter

+Contained by: component-specification

+

+Defines a formal parameter for the component.

+

+Attributes:

+  name: A unique name for the parameter.

+  type: The name of a Java class or primitive type expected by the parameter.

+  required: If yes, then the parameter must be bound.  If no (the default),

+    then the parameter is optional.

+  property-name: The name to use, instead of the parameter name, for the

+    JavaBean property connected to this parameter.

+  direction: The normal flow of data through the component 

+  			(in, form, custom, auto).

+  default-value: Specifies the default value for the parameter, 

+  				if the parameter is not bound.

+-->

+<!ELEMENT parameter (description?)>

+<!ATTLIST parameter

+	name CDATA #REQUIRED

+	type CDATA #IMPLIED

+	required %attribute-flag; "no"

+	property-name CDATA #IMPLIED

+	default-value CDATA #IMPLIED

+	direction (in | form | custom | auto) "custom"

+>

+<!-- =======================================================

+Element: private-asset

+Contained by: component-specification, page-specification

+

+An asset available within the Java classpath (i.e., bundled inside a JAR or WAR).

+

+Attributes:

+  name: The name of the asset.

+  resource-path: The complete pathname of the resource.

+-->

+<!ELEMENT private-asset (property*)>

+<!ATTLIST private-asset

+	name CDATA #REQUIRED

+	resource-path CDATA #REQUIRED

+>

+<!-- =======================================================

+Element: property

+Contained by: (many other elements)

+

+Defines a key/value pair associated with the application or component specification.  Properties

+are used to capture information that doesn't fit into the DTD.  The value for the property is

+the PCDATA wrapped by the property tag (which is trimmed of leading and trailing whitespace).

+

+This should not be confused with several other tags which are used to set JavaBeans properties

+of various objects.  The <property> tag exists to allow meta-data to be stored in the specification.

+

+Attributes:

+  name: The name of the property to set.

+  value: If specified, is the value of the property, otherwise, the PCDATA is used.

+  

+-->

+<!ELEMENT property (#PCDATA)>

+<!ATTLIST property

+	name CDATA #REQUIRED

+	value CDATA #IMPLIED

+>

+<!-- =======================================================

+Element: property-specification

+Appears in: page-specification, component-specification

+

+Identifies a transient or persistent property.

+

+Attributes:

+  name: The name of the property.

+  type: The type of the value, either the name of a scalar type,

+    or the fully qualified name of a class.  If omitted,

+    java.lang.Object is used.

+  persistent: If "yes", the value will be made persistant.  Default

+    is "no".

+  initial-value:  If provided, this is an OGNL expression used

+    to initialize the property.  If not specified, the

+    body of the element is used as the initial value.

+    

+-->

+<!ELEMENT property-specification (#PCDATA)>

+<!ATTLIST property-specification

+	name CDATA #REQUIRED

+	type CDATA #IMPLIED

+	persistent %attribute-flag; "no"

+	initial-value CDATA #IMPLIED

+>

+<!-- =======================================================

+Element: reserved-parameter

+Appears in: component-specification

+

+Identifies a name which may not be used as an informal parameter.

+Informal parameters are typically HTML attribute names; this

+list identifies HTML attributes that are written exclusively

+by the component and may not be affected by informal parameters.

+

+Attributes:

+  name: The parameter name to reserve.

+-->

+<!ELEMENT reserved-parameter EMPTY>

+<!ATTLIST reserved-parameter

+	name CDATA #REQUIRED

+>

+<!-- =======================================================

+Element: service

+Appears in: application

+

+Defines an engine service.  You may override the default

+set of engine services or provide completely new services.

+

+Attributes:

+  name: The name of the service.

+  class: The Java class to instantiate for the service.

+

+-->

+<!ELEMENT service EMPTY>

+<!ATTLIST service

+	name CDATA #REQUIRED

+	class CDATA #REQUIRED

+>

+<!-- =======================================================

+Element: set-property

+Appears in: bean

+

+Used to initialize a property of a helper bean.  An OGNL expression

+is provided as the expression attribute, or as wrapped

+character data.

+

+Attributes:

+  name: The name of the property to be set.

+  expression: The OGNL expression that provides a value.

+-->

+<!ELEMENT set-property (#PCDATA)>

+<!ATTLIST set-property

+	name CDATA #REQUIRED

+	expression CDATA #IMPLIED

+>

+<!-- =======================================================

+Element: set-message-property

+Appears in: bean

+

+A localized string.

+

+Attributes:

+  key:  Sets a property of a string from a localized string.

+

+-->

+<!ELEMENT set-message-property EMPTY>

+<!ATTLIST set-message-property

+	name CDATA #REQUIRED

+	key CDATA #REQUIRED

+>

+<!-- =======================================================

+Element: static-binding

+Appears in: component

+

+Binds a parameter of the component to a static value defined directly

+within this specification. The value either the value ttribute, or

+the PCDATA wrapped by the element (with leading and trailing whitespace removed).

+

+Attributes:

+  name: The name of the component parameter to bind.

+  value: The value of the binding.  If not specied, the PCDATA wrapped

+    by the element is the binding.

+-->

+<!ELEMENT static-binding (#PCDATA)>

+<!ATTLIST static-binding

+	name CDATA #REQUIRED

+	value CDATA #IMPLIED

+>

+<!-- =======================================================

+Element: message-binding

+Appears in: component

+

+Binds a parameter of the component to a localized message of

+its container.

+

+Attributes:

+  name: The name of the component parameter to bind.

+  key: The key used to access a localized string.

+  

+-->

+<!ELEMENT message-binding EMPTY>

+<!ATTLIST message-binding

+	name CDATA #REQUIRED

+	key CDATA #REQUIRED

+>

diff --git a/tapestry-framework/src/org/apache/tapestry/parse/TemplateAttribute.java b/tapestry-framework/src/org/apache/tapestry/parse/TemplateAttribute.java
new file mode 100644
index 0000000..3be1a12
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/TemplateAttribute.java
@@ -0,0 +1,59 @@
+//  Copyright 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.tapestry.parse;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+/**
+ *  An attribute, associated with a {@link org.apache.tapestry.parse.OpenToken}, taken
+ *  from a template.  Each attribute has a type and a value.  The interpretation of the
+ *  value is based on the type.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class TemplateAttribute
+{
+	private AttributeType _type;
+	private String _value;
+	
+	public TemplateAttribute(AttributeType type, String value)
+	{
+		_type = type;
+		_value = value;
+	}
+	
+	public AttributeType getType()
+	{
+		return _type;
+	}
+	
+	public String getValue()
+	{
+		return _value;
+	}
+	
+	public String toString()
+	{
+		ToStringBuilder builder = new ToStringBuilder(this);
+		builder.append("type", _type);
+		builder.append("value", _value);
+		
+		return builder.toString();
+	}
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/TemplateParseException.java b/tapestry-framework/src/org/apache/tapestry/parse/TemplateParseException.java
new file mode 100644
index 0000000..5c861fe
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/TemplateParseException.java
@@ -0,0 +1,63 @@
+//  Copyright 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.tapestry.parse;
+
+import org.apache.tapestry.ILocatable;
+import org.apache.tapestry.ILocation;
+
+/**
+ *  Exception thrown indicating a problem parsing an HTML template.
+ *
+ *  @author Howard Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class TemplateParseException extends Exception implements ILocatable
+{
+    private ILocation _location;
+    private Throwable _rootCause;
+
+    public TemplateParseException(String message)
+    {
+        this(message, null, null);
+    }
+
+    public TemplateParseException(String message, ILocation location)
+    {
+        this(message, location, null);
+    }
+
+    public TemplateParseException(String message, ILocation location, Throwable rootCause)
+    {
+        super(message);
+
+        _location = location;
+
+        _rootCause = rootCause;
+
+    }
+
+    public ILocation getLocation()
+    {
+        return _location;
+    }
+
+    public Throwable getRootCause()
+    {
+        return _rootCause;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/TemplateParser.java b/tapestry-framework/src/org/apache/tapestry/parse/TemplateParser.java
new file mode 100644
index 0000000..0e92805
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/TemplateParser.java
@@ -0,0 +1,1615 @@
+//  Copyright 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.tapestry.parse;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.oro.text.regex.MalformedPatternException;
+import org.apache.oro.text.regex.MatchResult;
+import org.apache.oro.text.regex.Pattern;
+import org.apache.oro.text.regex.PatternMatcher;
+import org.apache.oro.text.regex.Perl5Compiler;
+import org.apache.oro.text.regex.Perl5Matcher;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.Location;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.util.IdAllocator;
+
+/**
+ *  Parses Tapestry templates, breaking them into a series of
+ *  {@link org.apache.tapestry.parse.TemplateToken tokens}.
+ *  Although often referred to as an "HTML template", there is no real
+ *  requirement that the template be HTML.  This parser can handle
+ *  any reasonable SGML derived markup (including XML), 
+ *  but specifically works around the ambiguities
+ *  of HTML reasonably.
+ * 
+ *  <p>Dynamic markup in Tapestry attempts to be invisible.
+ *  Components are arbitrary tags containing a <code>jwcid</code> attribute.
+ *  Such components must be well balanced (have a matching close tag, or
+ *  end the tag with "<code>/&gt;</code>".
+ * 
+ *  <p>Generally, the id specified in the template is matched against
+ *  an component defined in the specification.  However, implicit
+ *  components are also possible.  The jwcid attribute uses
+ *  the syntax "<code>@Type</code>" for implicit components.
+ *  Type is the component type, and may include a library id prefix.  Such
+ *  a component is anonymous (but is given a unique id).
+ * 
+ *  <p>
+ *  (The unique ids assigned start with a dollar sign, which is normally
+ *  no allowed for component ids ... this helps to make them stand out
+ *  and assures that they do not conflict with
+ *  user-defined component ids.  These ids tend to propagate
+ *  into URLs and become HTML element names and even JavaScript
+ *  variable names ... the dollar sign is acceptible in these contexts as 
+ *  well).
+ * 
+ *  <p>Implicit component may also be given a name using the syntax
+ *  "<code>componentId:@Type</code>".  Such a component should
+ *  <b>not</b> be defined in the specification, but may still be
+ *  accessed via {@link org.apache.tapestry.IComponent#getComponent(String)}.
+ * 
+ *  <p>
+ *  Both defined and implicit components may have additional attributes
+ *  defined, simply by including them in the template.  They set formal or
+ *  informal parameters of the component to static strings.
+ *  {@link org.apache.tapestry.spec.IComponentSpecification#getAllowInformalParameters()},
+ *  if false, will cause such attributes to be simply ignored.  For defined
+ *  components, conflicting values defined in the template are ignored.
+ * 
+ *  <p>Attributes in component tags will become formal and informal parameters
+ *  of the corresponding component.  Most attributes will be
+ *
+ *  <p>The parser removes
+ *  the body of some tags (when the corresponding component doesn't
+ *  {@link org.apache.tapestry.spec.IComponentSpecification#getAllowBody() allow a body},
+ *  and allows
+ *  portions of the template to be completely removed.
+ *
+ *  <p>The parser does a pretty thorough lexical analysis of the template,
+ *  and reports a great number of errors, including improper nesting
+ *  of tags.
+ *
+ *  <p>The parser supports <em>invisible localization</em>:
+ *  The parser recognizes HTML of the form:
+ *  <code>&lt;span key="<i>value</i>"&gt; ... &lt;/span&gt;</code>
+ *  and converts them into a {@link TokenType#LOCALIZATION}
+ *  token.  You may also specifify a <code>raw</code> attribute ... if the value
+ *  is <code>true</code>, then the localized value is 
+ *  sent to the client without filtering, which is appropriate if the
+ *  value has any markup that should not be escaped.
+ *
+ *  @author Howard Lewis Ship, Geoff Longman
+ *  @version $Id$
+ * 
+ **/
+
+public class TemplateParser
+{
+    /**
+     *  A Factory used by {@link org.apache.tapestry.parse.TemplateParser} to create 
+     *  {@link org.apache.tapestry.parse.TemplateToken} objects.
+     * 
+     *  <p>
+     *  This class is extended by Spindle - the Eclipse Plugin for Tapestry.
+     *  <p>
+     *  @author glongman@intelligentworks.com
+     *  @since 3.0
+     */
+    protected static class TemplateTokenFactory
+    {
+
+        public OpenToken createOpenToken(String tagName, String jwcId, String type, ILocation location)
+        {
+            return new OpenToken(tagName, jwcId, type, location);
+        }
+
+        public CloseToken createCloseToken(String tagName, ILocation location)
+        {
+            return new CloseToken(tagName, location);
+        }
+
+        public TextToken createTextToken(char[] templateData, int blockStart, int end, ILocation templateLocation)
+        {
+            return new TextToken(templateData, blockStart, end, templateLocation);
+        }
+
+        public LocalizationToken createLocalizationToken(
+            String tagName,
+            String localizationKey,
+            boolean raw,
+            Map attributes,
+            ILocation startLocation)
+        {
+            return new LocalizationToken(tagName, localizationKey, raw, attributes, startLocation);
+        }
+    }
+
+    /**
+     *  Attribute value prefix indicating that the attribute is an OGNL expression.
+     * 
+     *  @since 3.0
+     **/
+
+    public static final String OGNL_EXPRESSION_PREFIX = "ognl:";
+
+    /**
+     *  Attribute value prefix indicating that the attribute is a localization
+     *  key.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    public static final String LOCALIZATION_KEY_PREFIX = "message:";
+
+    /**
+     *  A "magic" component id that causes the tag with the id and its entire
+     *  body to be ignored during parsing.
+     *
+     **/
+
+    private static final String REMOVE_ID = "$remove$";
+
+    /**
+     * A "magic" component id that causes the tag to represent the true
+     * content of the template.  Any content prior to the tag is discarded,
+     * and any content after the tag is ignored.  The tag itself is not
+     * included.
+     *
+     **/
+
+    private static final String CONTENT_ID = "$content$";
+
+    /**
+     *  
+     *  The attribute, checked for in &lt;span&gt; tags, that signfies
+     *  that the span is being used as an invisible localization.
+     * 
+     *  @since 2.0.4
+     * 
+     **/
+
+    public static final String LOCALIZATION_KEY_ATTRIBUTE_NAME = "key";
+
+    /**
+     *  Used with {@link #LOCALIZATION_KEY_ATTRIBUTE_NAME} to indicate a string
+     *  that should be rendered "raw" (without escaping HTML).  If not specified,
+     *  defaults to "false".  The value must equal "true" (caselessly).
+     * 
+     *  @since 2.3
+     * 
+     **/
+
+    public static final String RAW_ATTRIBUTE_NAME = "raw";
+
+    /**
+     *  Attribute used to identify components.
+     * 
+     *  @since 2.3
+     * 
+     **/
+
+    public static final String JWCID_ATTRIBUTE_NAME = "jwcid";
+
+    private static final String PROPERTY_NAME_PATTERN = "_?[a-zA-Z]\\w*";
+
+    /**
+     *  Pattern used to recognize ordinary components (defined in the specification).
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    public static final String SIMPLE_ID_PATTERN = "^(" + PROPERTY_NAME_PATTERN + ")$";
+
+    /**
+     *  Pattern used to recognize implicit components (whose type is defined in
+     *  the template).  Subgroup 1 is the id (which may be null) and subgroup 2
+     *  is the type (which may be qualified with a library prefix).
+     *  Subgroup 4 is the library id, Subgroup 5 is the simple component type.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    public static final String IMPLICIT_ID_PATTERN =
+        "^(" + PROPERTY_NAME_PATTERN + ")?@(((" + PROPERTY_NAME_PATTERN + "):)?(" + PROPERTY_NAME_PATTERN + "))$";
+
+    private static final int IMPLICIT_ID_PATTERN_ID_GROUP = 1;
+    private static final int IMPLICIT_ID_PATTERN_TYPE_GROUP = 2;
+    private static final int IMPLICIT_ID_PATTERN_LIBRARY_ID_GROUP = 4;
+    private static final int IMPLICIT_ID_PATTERN_SIMPLE_TYPE_GROUP = 5;
+
+    private Pattern _simpleIdPattern;
+    private Pattern _implicitIdPattern;
+    private PatternMatcher _patternMatcher;
+
+    private IdAllocator _idAllocator = new IdAllocator();
+
+    private ITemplateParserDelegate _delegate;
+
+    /**
+     *  Identifies the template being parsed; used with error messages.
+     *
+     **/
+
+    private IResourceLocation _resourceLocation;
+
+    /**
+     *  Shared instance of {@link Location} used by
+     *  all {@link TextToken} instances in the template.
+     * 
+     **/
+
+    private ILocation _templateLocation;
+
+    /**
+     *  Location with in the resource for the current line.
+     * 
+     **/
+
+    private ILocation _currentLocation;
+
+    /**
+     *  Local reference to the template data that is to be parsed.
+     *
+     **/
+
+    private char[] _templateData;
+
+    /**
+     *  List of Tag
+     *
+     **/
+
+    private List _stack = new ArrayList();
+
+    private static class Tag
+    {
+        // The element, i.e., <jwc> or virtually any other element (via jwcid attribute)
+        String _tagName;
+        // If true, the tag is a placeholder for a dynamic element
+        boolean _component;
+        // If true, the body of the tag is being ignored, and the
+        // ignore flag is cleared when the close tag is reached
+        boolean _ignoringBody;
+        // If true, then the entire tag (and its body) is being ignored
+        boolean _removeTag;
+        // If true, then the tag must have a balanced closing tag.
+        // This is always true for components.
+        boolean _mustBalance;
+        // The line on which the start tag exists
+        int _line;
+        // If true, then the parse ends when the closing tag is found.
+        boolean _content;
+
+        Tag(String tagName, int line)
+        {
+            _tagName = tagName;
+            _line = line;
+        }
+
+        boolean match(String matchTagName)
+        {
+            return _tagName.equalsIgnoreCase(matchTagName);
+        }
+    }
+
+    /**
+     *  List of {@link TemplateToken}, this forms the ultimate response.
+     *
+     **/
+
+    private List _tokens = new ArrayList();
+
+    /**
+     *  The location of the 'cursor' within the template data.  The
+     *  advance() method moves this forward.
+     *
+     **/
+
+    private int _cursor;
+
+    /**
+     *  The start of the current block of static text, or -1 if no block
+     *  is active.
+     *
+     **/
+
+    private int _blockStart;
+
+    /**
+     *  The current line number; tracked by advance().  Starts at 1.
+     *
+     **/
+
+    private int _line;
+
+    /**
+     *  Set to true when the body of a tag is being ignored.  This is typically
+     *  used to skip over the body of a tag when its corresponding
+     *  component doesn't allow a body, or whe the special
+     *  jwcid of $remove$ is used.
+     *
+     **/
+
+    private boolean _ignoring;
+
+    /**
+     *  A {@link Map} of {@link String}s, used to store attributes collected
+     *  while parsing a tag.
+     *
+     **/
+
+    private Map _attributes = new HashMap();
+
+    /**
+     *  A factory used to create template tokens.
+     * <p>
+     * author glongman@intelligentworks.com
+     */
+
+    protected TemplateTokenFactory _factory;
+
+    public TemplateParser()
+    {
+        Perl5Compiler compiler = new Perl5Compiler();
+
+        try
+        {
+            _simpleIdPattern = compiler.compile(SIMPLE_ID_PATTERN);
+            _implicitIdPattern = compiler.compile(IMPLICIT_ID_PATTERN);
+        } catch (MalformedPatternException ex)
+        {
+            throw new ApplicationRuntimeException(ex);
+        }
+
+        _patternMatcher = new Perl5Matcher();
+
+        _factory = new TemplateTokenFactory();
+    }
+
+    /**
+     *  Parses the template data into an array of {@link TemplateToken}s.
+     *
+     *  <p>The parser is <i>decidedly</i> not threadsafe, so care should be taken
+     *  that only a single thread accesses it.
+     *
+     *  @param templateData the HTML template to parse.  Some tokens will hold
+     *  a reference to this array.
+     *  @param delegate  object that "knows" about defined components
+     *  @param resourceLocation a description of where the template originated from,
+     *  used with error messages.
+     *
+     **/
+
+    public TemplateToken[] parse(
+        char[] templateData,
+        ITemplateParserDelegate delegate,
+        IResourceLocation resourceLocation)
+        throws TemplateParseException
+    {
+        TemplateToken[] result = null;
+
+        try
+        {
+            beforeParse(templateData, delegate, resourceLocation);
+
+            parse();
+
+            result = (TemplateToken[]) _tokens.toArray(new TemplateToken[_tokens.size()]);
+        } finally
+        {
+            afterParse();
+        }
+
+        return result;
+    }
+
+    /**
+     *  perform default initialization of the parser.
+     *  <p>
+     *  author glongman@intelligentworks.com
+     */
+
+    protected void beforeParse(
+        char[] templateData,
+        ITemplateParserDelegate delegate,
+        IResourceLocation resourceLocation)
+    {
+        _templateData = templateData;
+        _resourceLocation = resourceLocation;
+        _templateLocation = new Location(resourceLocation);
+        _delegate = delegate;
+        _ignoring = false;
+        _line = 1;
+    }
+
+    /**
+     *  Perform default cleanup after parsing completes.
+     *  <p>
+     *  author glongman@intelligentworks.com
+     */
+
+    protected void afterParse()
+    {
+        _delegate = null;
+        _templateData = null;
+        _resourceLocation = null;
+        _templateLocation = null;
+        _currentLocation = null;
+        _stack.clear();
+        _tokens.clear();
+        _attributes.clear();
+        _idAllocator.clear();
+    }
+
+    /**
+     * Used by the parser to report problems in the parse.
+     * Parsing <b>must</b> stop when a problem is reported.
+     * <p>
+     * The default implementation simply throws an exception that contains
+     * the message and location parameters.
+     * <p>
+     * Subclasses may override but <b>must</b> ensure they throw the required exception.
+     * <p>
+     *
+     * author glongman@intelligentworks.com
+     *  
+     * @param message
+     * @param location
+     * @param line ignored by the default impl
+     * @param cursor ignored by the default impl
+     * @throws TemplateParseException always thrown in order to terminate the parse.
+     */
+
+    protected void templateParseProblem(String message, ILocation location, int line, int cursor)
+        throws TemplateParseException
+    {
+        throw new TemplateParseException(message, location);
+    }
+
+    /**
+     * Used by the parser to report tapestry runtime specific problems in the parse.
+     * Parsing <b>must</b> stop when a problem is reported.
+     * <p>
+     * The default implementation simply rethrows the exception.
+     * <p>
+     * Subclasses may override but <b>must</b> ensure they rethrow the exception.
+     * <p>
+     *
+     * author glongman@intelligentworks.com
+     *  
+     * @param exception
+     * @param line ignored by the default impl
+     * @param cursor ignored by the default impl
+     * @throws ApplicationRuntimeException always rethrown in order to terminate the parse.
+     */
+
+    protected void templateParseProblem(ApplicationRuntimeException exception, int line, int cursor)
+        throws ApplicationRuntimeException
+    {
+        throw exception;
+    }
+
+    /**
+     * Give subclasses access to the parse results.
+     * <p>
+     *
+     * author glongman@intelligentworks.com
+     */
+    protected List getTokens()
+    {
+        if (_tokens == null)
+            return Collections.EMPTY_LIST;
+
+        return _tokens;
+    }
+
+    /**
+     *  Checks to see if the next few characters match a given pattern.
+     *
+     **/
+
+    private boolean lookahead(char[] match)
+    {
+        try
+        {
+            for (int i = 0; i < match.length; i++)
+            {
+                if (_templateData[_cursor + i] != match[i])
+                    return false;
+            }
+
+            // Every character matched.
+
+            return true;
+        } catch (IndexOutOfBoundsException ex)
+        {
+            return false;
+        }
+    }
+
+    private static final char[] COMMENT_START = new char[] { '<', '!', '-', '-' };
+    private static final char[] COMMENT_END = new char[] { '-', '-', '>' };
+    private static final char[] CLOSE_TAG = new char[] { '<', '/' };
+
+    protected void parse() throws TemplateParseException
+    {
+        _cursor = 0;
+        _blockStart = -1;
+        int length = _templateData.length;
+
+        while (_cursor < length)
+        {
+            if (_templateData[_cursor] != '<')
+            {
+                if (_blockStart < 0 && !_ignoring)
+                    _blockStart = _cursor;
+
+                advance();
+                continue;
+            }
+
+            // OK, start of something.
+
+            if (lookahead(CLOSE_TAG))
+            {
+                closeTag();
+                continue;
+            }
+
+            if (lookahead(COMMENT_START))
+            {
+                skipComment();
+                continue;
+            }
+
+            // The start of some tag.
+
+            startTag();
+        }
+
+        // Usually there's some text at the end of the template (after the last closing tag) that should
+        // be added.  Often the last few tags are static tags so we definately
+        // need to end the text block.
+
+        addTextToken(_templateData.length - 1);
+    }
+
+    /**
+     *  Advance forward in the document until the end of the comment is reached.
+     *  In addition, skip any whitespace following the comment.
+     *
+     **/
+
+    private void skipComment() throws TemplateParseException
+    {
+        int length = _templateData.length;
+        int startLine = _line;
+
+        if (_blockStart < 0 && !_ignoring)
+            _blockStart = _cursor;
+
+        while (true)
+        {
+            if (_cursor >= length)
+                templateParseProblem(
+                    Tapestry.format("TemplateParser.comment-not-ended", Integer.toString(startLine)),
+                    new Location(_resourceLocation, startLine),
+                    startLine,
+                    _cursor);
+
+            if (lookahead(COMMENT_END))
+                break;
+
+            // Not the end of the comment, advance over it.
+
+            advance();
+        }
+
+        _cursor += COMMENT_END.length;
+        advanceOverWhitespace();
+    }
+
+    private void addTextToken(int end)
+    {
+        // No active block to add to.
+
+        if (_blockStart < 0)
+            return;
+
+        if (_blockStart <= end)
+        {
+            TemplateToken token = _factory.createTextToken(_templateData, _blockStart, end, _templateLocation);
+
+            _tokens.add(token);
+        }
+
+        _blockStart = -1;
+    }
+
+    private static final int WAIT_FOR_ATTRIBUTE_NAME = 0;
+    private static final int COLLECT_ATTRIBUTE_NAME = 1;
+    private static final int ADVANCE_PAST_EQUALS = 2;
+    private static final int WAIT_FOR_ATTRIBUTE_VALUE = 3;
+    private static final int COLLECT_QUOTED_VALUE = 4;
+    private static final int COLLECT_UNQUOTED_VALUE = 5;
+
+    private void startTag() throws TemplateParseException
+    {
+        int cursorStart = _cursor;
+        int length = _templateData.length;
+        String tagName = null;
+        boolean endOfTag = false;
+        boolean emptyTag = false;
+        int startLine = _line;
+        ILocation startLocation = new Location(_resourceLocation, startLine);
+
+        tagBeginEvent(startLine, _cursor);
+
+        advance();
+
+        // Collect the element type
+
+        while (_cursor < length)
+        {
+            char ch = _templateData[_cursor];
+
+            if (ch == '/' || ch == '>' || Character.isWhitespace(ch))
+            {
+                tagName = new String(_templateData, cursorStart + 1, _cursor - cursorStart - 1);
+
+                break;
+            }
+
+            advance();
+        }
+
+        String attributeName = null;
+        int attributeNameStart = -1;
+        int attributeValueStart = -1;
+        int state = WAIT_FOR_ATTRIBUTE_NAME;
+        char quoteChar = 0;
+
+        _attributes.clear();
+
+        // Collect each attribute
+
+        while (!endOfTag)
+        {
+            if (_cursor >= length)
+            {
+                String key = (tagName == null) ? "TemplateParser.unclosed-unknown-tag" : "TemplateParser.unclosed-tag";
+
+                templateParseProblem(
+                    Tapestry.format(key, tagName, Integer.toString(startLine)),
+                    startLocation,
+                    startLine,
+                    cursorStart);
+
+            }
+
+            char ch = _templateData[_cursor];
+
+            switch (state)
+            {
+                case WAIT_FOR_ATTRIBUTE_NAME :
+
+                    // Ignore whitespace before the next attribute name, while
+                    // looking for the end of the current tag.
+
+                    if (ch == '/')
+                    {
+                        emptyTag = true;
+                        advance();
+                        break;
+                    }
+
+                    if (ch == '>')
+                    {
+                        endOfTag = true;
+                        break;
+                    }
+
+                    if (Character.isWhitespace(ch))
+                    {
+                        advance();
+                        break;
+                    }
+
+                    // Found non-whitespace, assume its the attribute name.
+                    // Note: could use a check here for non-alpha.
+
+                    attributeNameStart = _cursor;
+                    state = COLLECT_ATTRIBUTE_NAME;
+                    advance();
+                    break;
+
+                case COLLECT_ATTRIBUTE_NAME :
+
+                    // Looking for end of attribute name.
+
+                    if (ch == '=' || ch == '/' || ch == '>' || Character.isWhitespace(ch))
+                    {
+                        attributeName = new String(_templateData, attributeNameStart, _cursor - attributeNameStart);
+
+                        state = ADVANCE_PAST_EQUALS;
+                        break;
+                    }
+
+                    // Part of the attribute name
+
+                    advance();
+                    break;
+
+                case ADVANCE_PAST_EQUALS :
+
+                    // Looking for the '=' sign.  May hit the end of the tag, or (for bare attributes),
+                    // the next attribute name.
+
+                    if (ch == '/' || ch == '>')
+                    {
+                        // A bare attribute, which is not interesting to
+                        // us.
+
+                        state = WAIT_FOR_ATTRIBUTE_NAME;
+                        break;
+                    }
+
+                    if (Character.isWhitespace(ch))
+                    {
+                        advance();
+                        break;
+                    }
+
+                    if (ch == '=')
+                    {
+                        state = WAIT_FOR_ATTRIBUTE_VALUE;
+                        quoteChar = 0;
+                        attributeValueStart = -1;
+                        advance();
+                        break;
+                    }
+
+                    // Otherwise, an HTML style "bare" attribute (such as <select multiple>).
+                    // We aren't interested in those (we're just looking for the id or jwcid attribute).
+
+                    state = WAIT_FOR_ATTRIBUTE_NAME;
+                    break;
+
+                case WAIT_FOR_ATTRIBUTE_VALUE :
+
+                    if (ch == '/' || ch == '>')
+                        templateParseProblem(
+                            Tapestry.format(
+                                "TemplateParser.missing-attribute-value",
+                                tagName,
+                                Integer.toString(_line),
+                                attributeName),
+                            getCurrentLocation(),
+                            _line,
+                            _cursor);
+
+                    // Ignore whitespace between '=' and the attribute value.  Also, look
+                    // for initial quote.
+
+                    if (Character.isWhitespace(ch))
+                    {
+                        advance();
+                        break;
+                    }
+
+                    if (ch == '\'' || ch == '"')
+                    {
+                        quoteChar = ch;
+
+                        state = COLLECT_QUOTED_VALUE;
+                        advance();
+                        attributeValueStart = _cursor;
+                        attributeBeginEvent(attributeName, _line, attributeValueStart);
+                        break;
+                    }
+
+                    // Not whitespace or quote, must be start of unquoted attribute.
+
+                    state = COLLECT_UNQUOTED_VALUE;
+                    attributeValueStart = _cursor;
+                    attributeBeginEvent(attributeName, _line, attributeValueStart);
+                    break;
+
+                case COLLECT_QUOTED_VALUE :
+
+                    // Start collecting the quoted attribute value.  Stop at the matching quote character,
+                    // unless bare, in which case, stop at the next whitespace.
+
+                    if (ch == quoteChar)
+                    {
+                        String attributeValue =
+                            new String(_templateData, attributeValueStart, _cursor - attributeValueStart);
+                        
+                       
+                        attributeEndEvent(_cursor);
+                        
+                        if (_attributes.containsKey(attributeName))
+                        	templateParseProblem(
+                                    Tapestry.format(
+                                        "TemplateParser.duplicate-tag-attribute",
+                                        tagName,
+                                        Integer.toString(_line),
+                                        attributeName),
+                                    getCurrentLocation(),
+                                    _line,
+                                    _cursor);
+                            
+                          _attributes.put(attributeName, attributeValue);
+
+                        // Advance over the quote.
+                        advance();
+                        state = WAIT_FOR_ATTRIBUTE_NAME;
+                        break;
+                    }
+
+                    advance();
+                    break;
+
+                case COLLECT_UNQUOTED_VALUE :
+
+                    // An unquoted attribute value ends with whitespace 
+                    // or the end of the enclosing tag.
+
+                    if (ch == '/' || ch == '>' || Character.isWhitespace(ch))
+                    {
+                        String attributeValue =
+                            new String(_templateData, attributeValueStart, _cursor - attributeValueStart);
+
+                        attributeEndEvent(_cursor);
+                        
+                        if (_attributes.containsKey(attributeName))
+                        	templateParseProblem(
+                                    Tapestry.format(
+                                        "TemplateParser.duplicate-tag-attribute",
+                                        tagName,
+                                        Integer.toString(_line),
+                                        attributeName),
+                                    getCurrentLocation(),
+                                    _line,
+                                    _cursor);
+                            
+                          _attributes.put(attributeName, attributeValue);
+
+                        state = WAIT_FOR_ATTRIBUTE_NAME;
+                        break;
+                    }
+
+                    advance();
+                    break;
+            }
+        }
+        
+        tagEndEvent(_cursor);
+
+        // Check for invisible localizations
+
+        String localizationKey = findValueCaselessly(LOCALIZATION_KEY_ATTRIBUTE_NAME, _attributes);
+        String jwcId = findValueCaselessly(JWCID_ATTRIBUTE_NAME, _attributes);
+
+        if (localizationKey != null && tagName.equalsIgnoreCase("span") && jwcId == null)
+        {
+            if (_ignoring)
+                templateParseProblem(
+                    Tapestry.format(
+                        "TemplateParser.component-may-not-be-ignored",
+                        tagName,
+                        Integer.toString(startLine)),
+                    startLocation,
+                    startLine,
+                    cursorStart);
+
+            // If the tag isn't empty, then create a Tag instance to ignore the
+            // body of the tag.
+
+            if (!emptyTag)
+            {
+                Tag tag = new Tag(tagName, startLine);
+
+                tag._component = false;
+                tag._removeTag = true;
+                tag._ignoringBody = true;
+                tag._mustBalance = true;
+
+                _stack.add(tag);
+
+                // Start ignoring content until the close tag.
+
+                _ignoring = true;
+            } else
+            {
+                // Cursor is at the closing carat, advance over it and any whitespace.                
+                advance();
+                advanceOverWhitespace();
+            }
+
+            // End any open block.
+
+            addTextToken(cursorStart - 1);
+
+            boolean raw = checkBoolean(RAW_ATTRIBUTE_NAME, _attributes);
+
+            Map attributes = filter(_attributes, new String[] { LOCALIZATION_KEY_ATTRIBUTE_NAME, RAW_ATTRIBUTE_NAME });
+
+            TemplateToken token =
+                _factory.createLocalizationToken(tagName, localizationKey, raw, attributes, startLocation);
+
+            _tokens.add(token);
+
+            return;
+        }
+
+        if (jwcId != null)
+        {
+            processComponentStart(tagName, jwcId, emptyTag, startLine, cursorStart, startLocation);
+            return;
+        }
+
+        // A static tag (not a tag without a jwcid attribute).
+        // We need to record this so that we can match close tags later.
+
+        if (!emptyTag)
+        {
+            Tag tag = new Tag(tagName, startLine);
+            _stack.add(tag);
+        }
+
+        // If there wasn't an active block, then start one.
+
+        if (_blockStart < 0 && !_ignoring)
+            _blockStart = cursorStart;
+
+        advance();
+    }
+
+    /**
+     *  Processes a tag that is the open tag for a component (but also handles
+     *  the $remove$ and $content$ tags).
+     * 
+     **/
+
+    /**
+     * Notify that the beginning of a tag has been detected.
+     * <p>
+     * Default implementation does nothing.
+     * <p>
+     *
+     * author glongman@intelligentworks.com
+     */
+    protected void tagBeginEvent(int startLine, int cursorPosition)
+    {
+    }
+
+    /**
+     * Notify that the end of the current tag has been detected.
+     * <p>
+     * Default implementation does nothing.
+     * <p>
+     * author glongman@intelligentworks.com
+     */
+    protected void tagEndEvent(int cursorPosition)
+    {
+    }
+
+    /**
+     * Notify that the beginning of an attribute value has been detected.
+     * <p>
+     * Default implementation does nothing.
+     * <p>
+     * author glongman@intelligentworks.com
+     */
+    protected void attributeBeginEvent(String attributeName, int startLine, int cursorPosition)
+    {
+    }
+
+    /**
+     * Notify that the end of the current attribute value has been detected.
+     * <p>
+     * Default implementation does nothing.
+     * <p>
+     * author glongman@intelligentworks.com
+     */
+    protected void attributeEndEvent(int cursorPosition)
+    {
+    }
+
+    private void processComponentStart(
+        String tagName,
+        String jwcId,
+        boolean emptyTag,
+        int startLine,
+        int cursorStart,
+        ILocation startLocation)
+        throws TemplateParseException
+    {
+        if (jwcId.equalsIgnoreCase(CONTENT_ID))
+        {
+            processContentTag(tagName, startLine, cursorStart, emptyTag);
+
+            return;
+        }
+
+        boolean isRemoveId = jwcId.equalsIgnoreCase(REMOVE_ID);
+
+        if (_ignoring && !isRemoveId)
+            templateParseProblem(
+                Tapestry.format("TemplateParser.component-may-not-be-ignored", tagName, Integer.toString(startLine)),
+                startLocation,
+                startLine,
+                cursorStart);
+
+        String type = null;
+        boolean allowBody = false;
+
+        if (_patternMatcher.matches(jwcId, _implicitIdPattern))
+        {
+            MatchResult match = _patternMatcher.getMatch();
+
+            jwcId = match.group(IMPLICIT_ID_PATTERN_ID_GROUP);
+            type = match.group(IMPLICIT_ID_PATTERN_TYPE_GROUP);
+
+            String libraryId = match.group(IMPLICIT_ID_PATTERN_LIBRARY_ID_GROUP);
+            String simpleType = match.group(IMPLICIT_ID_PATTERN_SIMPLE_TYPE_GROUP);
+
+            // If (and this is typical) no actual component id was specified,
+            // then generate one on the fly.
+            // The allocated id for anonymous components is
+            // based on the simple (unprefixed) type, but starts
+            // with a leading dollar sign to ensure no conflicts
+            // with user defined component ids (which don't allow dollar signs
+            // in the id).
+
+            if (jwcId == null)
+                jwcId = _idAllocator.allocateId("$" + simpleType);
+
+            try
+            {
+                allowBody = _delegate.getAllowBody(libraryId, simpleType, startLocation);
+            } catch (ApplicationRuntimeException e)
+            {
+                // give subclasses a chance to handle and rethrow
+                templateParseProblem(e, startLine, cursorStart);
+            }
+
+        } else
+        {
+            if (!isRemoveId)
+            {
+                if (!_patternMatcher.matches(jwcId, _simpleIdPattern))
+                    templateParseProblem(
+                        Tapestry.format(
+                            "TemplateParser.component-id-invalid",
+                            tagName,
+                            Integer.toString(startLine),
+                            jwcId),
+                        startLocation,
+                        startLine,
+                        cursorStart);
+
+                if (!_delegate.getKnownComponent(jwcId))
+                    templateParseProblem(
+                        Tapestry.format(
+                            "TemplateParser.unknown-component-id",
+                            tagName,
+                            Integer.toString(startLine),
+                            jwcId),
+                        startLocation,
+                        startLine,
+                        cursorStart);
+
+                try
+                {
+                    allowBody = _delegate.getAllowBody(jwcId, startLocation);
+                } catch (ApplicationRuntimeException e)
+                {
+                    // give subclasses a chance to handle and rethrow
+                    templateParseProblem(e, startLine, cursorStart);
+                }
+            }
+        }
+
+        // Ignore the body if we're removing the entire tag,
+        // of if the corresponding component doesn't allow
+        // a body.
+
+        boolean ignoreBody = !emptyTag && (isRemoveId || !allowBody);
+
+        if (_ignoring && ignoreBody)
+            templateParseProblem(
+                Tapestry.format("TemplateParser.nested-ignore", tagName, Integer.toString(startLine)),
+                new Location(_resourceLocation, startLine),
+                startLine,
+                cursorStart);
+
+        if (!emptyTag)
+            pushNewTag(tagName, startLine, isRemoveId, ignoreBody);
+
+        // End any open block.
+
+        addTextToken(cursorStart - 1);
+
+        if (!isRemoveId)
+        {
+            addOpenToken(tagName, jwcId, type, startLocation);
+
+            if (emptyTag)
+                _tokens.add(_factory.createCloseToken(tagName, getCurrentLocation()));
+        }
+
+        advance();
+    }
+
+    private void pushNewTag(String tagName, int startLine, boolean isRemoveId, boolean ignoreBody)
+    {
+        Tag tag = new Tag(tagName, startLine);
+
+        tag._component = !isRemoveId;
+        tag._removeTag = isRemoveId;
+
+        tag._ignoringBody = ignoreBody;
+
+        _ignoring = tag._ignoringBody;
+
+        tag._mustBalance = true;
+
+        _stack.add(tag);
+    }
+
+    private void processContentTag(String tagName, int startLine, int cursorStart, boolean emptyTag)
+        throws TemplateParseException
+    {
+        if (_ignoring)
+            templateParseProblem(
+                Tapestry.format(
+                    "TemplateParser.content-block-may-not-be-ignored",
+                    tagName,
+                    Integer.toString(startLine)),
+                new Location(_resourceLocation, startLine),
+                startLine,
+                cursorStart);
+
+        if (emptyTag)
+            templateParseProblem(
+                Tapestry.format("TemplateParser.content-block-may-not-be-empty", tagName, Integer.toString(startLine)),
+                new Location(_resourceLocation, startLine),
+                startLine,
+                cursorStart);
+
+        _tokens.clear();
+        _blockStart = -1;
+
+        Tag tag = new Tag(tagName, startLine);
+
+        tag._mustBalance = true;
+        tag._content = true;
+
+        _stack.clear();
+        _stack.add(tag);
+
+        advance();
+    }
+
+    private void addOpenToken(String tagName, String jwcId, String type, ILocation location)
+    {
+        OpenToken token = _factory.createOpenToken(tagName, jwcId, type, location);
+        _tokens.add(token);
+
+        if (_attributes.isEmpty())
+            return;
+
+        Iterator i = _attributes.entrySet().iterator();
+        while (i.hasNext())
+        {
+            Map.Entry entry = (Map.Entry) i.next();
+
+            String key = (String) entry.getKey();
+
+            if (key.equalsIgnoreCase(JWCID_ATTRIBUTE_NAME))
+                continue;
+
+            String value = (String) entry.getValue();
+
+            addAttributeToToken(token, key, value);
+        }
+    }
+
+    /**
+     *  Analyzes the attribute value, looking for possible prefixes that indicate
+     *  the value is not a literal.  Adds the attribute to the
+     *  token.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    private void addAttributeToToken(OpenToken token, String name, String attributeValue)
+    {
+        int pos = attributeValue.indexOf(":");
+
+        if (pos > 0)
+        {
+
+            String prefix = attributeValue.substring(0, pos + 1);
+
+            if (prefix.equals(OGNL_EXPRESSION_PREFIX))
+            {
+                token.addAttribute(
+                    name,
+                    AttributeType.OGNL_EXPRESSION,
+                    extractExpression(attributeValue.substring(pos + 1)));
+                return;
+            }
+
+            if (prefix.equals(LOCALIZATION_KEY_PREFIX))
+            {
+                token.addAttribute(name, AttributeType.LOCALIZATION_KEY, attributeValue.substring(pos + 1).trim());
+                return;
+
+            }
+        }
+
+        token.addAttribute(name, AttributeType.LITERAL, attributeValue);
+    }
+
+    /**
+     *  Invoked to handle a closing tag, i.e., &lt;/foo&gt;.  When a tag closes, it will match against
+     *  a tag on the open tag start.  Preferably the top tag on the stack (if everything is well balanced), but this
+     *  is HTML, not XML, so many tags won't balance.
+     *
+     *  <p>Once the matching tag is located, the question is ... is the tag dynamic or static?  If static, then
+     * the current text block is extended to include this close tag.  If dynamic, then the current text block
+     * is ended (before the '&lt;' that starts the tag) and a close token is added.
+     *
+     * <p>In either case, the matching static element and anything above it is removed, and the cursor is left
+     * on the character following the '&gt;'.
+     *
+     **/
+
+    private void closeTag() throws TemplateParseException
+    {
+        int cursorStart = _cursor;
+        int length = _templateData.length;
+        int startLine = _line;
+
+        ILocation startLocation = getCurrentLocation();
+
+        _cursor += CLOSE_TAG.length;
+
+        int tagStart = _cursor;
+
+        while (true)
+        {
+            if (_cursor >= length)
+                templateParseProblem(
+                    Tapestry.format("TemplateParser.incomplete-close-tag", Integer.toString(startLine)),
+                    startLocation,
+                    startLine,
+                    cursorStart);
+
+            char ch = _templateData[_cursor];
+
+            if (ch == '>')
+                break;
+
+            advance();
+        }
+
+        String tagName = new String(_templateData, tagStart, _cursor - tagStart);
+
+        int stackPos = _stack.size() - 1;
+        Tag tag = null;
+
+        while (stackPos >= 0)
+        {
+            tag = (Tag) _stack.get(stackPos);
+
+            if (tag.match(tagName))
+                break;
+
+            if (tag._mustBalance)
+                templateParseProblem(
+                    Tapestry.format(
+                        "TemplateParser.improperly-nested-close-tag",
+                        new Object[] {
+                            tagName,
+                            Integer.toString(startLine),
+                            tag._tagName,
+                            Integer.toString(tag._line)}),
+                    startLocation,
+                    startLine,
+                    cursorStart);
+
+            stackPos--;
+        }
+
+        if (stackPos < 0)
+            templateParseProblem(
+                Tapestry.format("TemplateParser.unmatched-close-tag", tagName, Integer.toString(startLine)),
+                startLocation,
+                startLine,
+                cursorStart);
+
+        // Special case for the content tag
+
+        if (tag._content)
+        {
+            addTextToken(cursorStart - 1);
+
+            // Advance the cursor right to the end.
+
+            _cursor = length;
+            _stack.clear();
+            return;
+        }
+
+        // When a component closes, add a CLOSE tag.
+        if (tag._component)
+        {
+            addTextToken(cursorStart - 1);
+
+            _tokens.add(_factory.createCloseToken(tagName, getCurrentLocation()));
+        } else
+        {
+            // The close of a static tag.  Unless removing the tag
+            // entirely, make sure the block tag is part of a text block.
+
+            if (_blockStart < 0 && !tag._removeTag && !_ignoring)
+                _blockStart = cursorStart;
+        }
+
+        // Remove all elements at stackPos or above.
+
+        for (int i = _stack.size() - 1; i >= stackPos; i--)
+            _stack.remove(i);
+
+        // Advance cursor past '>'
+
+        advance();
+
+        // If editting out the tag (i.e., $remove$) then kill any whitespace.
+        // For components that simply don't contain a body, removeTag will
+        // be false.
+
+        if (tag._removeTag)
+            advanceOverWhitespace();
+
+        // If we were ignoring the body of the tag, then clear the ignoring
+        // flag, since we're out of the body.
+
+        if (tag._ignoringBody)
+            _ignoring = false;
+    }
+
+    /**
+     *  Advances the cursor to the next character.
+     *  If the end-of-line is reached, then increments
+     *  the line counter.
+     *
+     **/
+
+    private void advance()
+    {
+        int length = _templateData.length;
+
+        if (_cursor >= length)
+            return;
+
+        char ch = _templateData[_cursor];
+
+        _cursor++;
+
+        if (ch == '\n')
+        {
+            _line++;
+            _currentLocation = null;
+            return;
+        }
+
+        // A \r, or a \r\n also counts as a new line.
+
+        if (ch == '\r')
+        {
+            _line++;
+            _currentLocation = null;
+
+            if (_cursor < length && _templateData[_cursor] == '\n')
+                _cursor++;
+
+            return;
+        }
+
+        // Not an end-of-line character.
+
+    }
+
+    private void advanceOverWhitespace()
+    {
+        int length = _templateData.length;
+
+        while (_cursor < length)
+        {
+            char ch = _templateData[_cursor];
+            if (!Character.isWhitespace(ch))
+                return;
+
+            advance();
+        }
+    }
+
+    /**
+     *  Returns a new Map that is a copy of the input Map with some
+     *  key/value pairs removed.  A list of keys is passed in
+     *  and matching keys (caseless comparison) from the input
+     *  Map are excluded from the output map.  May return null
+     *  (rather than return an empty Map).
+     * 
+     **/
+
+    private Map filter(Map input, String[] removeKeys)
+    {
+        if (input == null || input.isEmpty())
+            return null;
+
+        Map result = null;
+
+        Iterator i = input.entrySet().iterator();
+
+        nextkey : while (i.hasNext())
+        {
+            Map.Entry entry = (Map.Entry) i.next();
+
+            String key = (String) entry.getKey();
+
+            for (int j = 0; j < removeKeys.length; j++)
+            {
+                if (key.equalsIgnoreCase(removeKeys[j]))
+                    continue nextkey;
+            }
+
+            if (result == null)
+                result = new HashMap(input.size());
+
+            result.put(key, entry.getValue());
+        }
+
+        return result;
+    }
+
+    /**
+     *  Searches a Map for given key, caselessly.  The Map is expected to consist of Strings for keys and
+     *  values.  Returns the value for the first key found that matches (caselessly) the input key.  Returns null
+     *  if no value found.
+     * 
+     **/
+
+    protected String findValueCaselessly(String key, Map map)
+    {
+        String result = (String) map.get(key);
+
+        if (result != null)
+            return result;
+
+        Iterator i = map.entrySet().iterator();
+        while (i.hasNext())
+        {
+            Map.Entry entry = (Map.Entry) i.next();
+
+            String entryKey = (String) entry.getKey();
+
+            if (entryKey.equalsIgnoreCase(key))
+                return (String) entry.getValue();
+        }
+
+        return null;
+    }
+
+    /**
+     *  Conversions needed by {@link #extractExpression(String)}
+     * 
+     **/
+
+    private static final String[] CONVERSIONS = { "&lt;", "<", "&gt;", ">", "&quot;", "\"", "&amp;", "&" };
+
+    /**
+     *  Provided a raw input string that has been recognized to be an expression,
+     *  this removes excess white space and converts &amp;amp;, &amp;quot; &amp;lt; and &amp;gt;
+     *  to their normal character values (otherwise its impossible to specify
+     *  those values in expressions in the template).
+     * 
+     **/
+
+    private String extractExpression(String input)
+    {
+        int inputLength = input.length();
+
+        StringBuffer buffer = new StringBuffer(inputLength);
+
+        int cursor = 0;
+
+        outer : while (cursor < inputLength)
+        {
+            for (int i = 0; i < CONVERSIONS.length; i += 2)
+            {
+                String entity = CONVERSIONS[i];
+                int entityLength = entity.length();
+                String value = CONVERSIONS[i + 1];
+
+                if (cursor + entityLength > inputLength)
+                    continue;
+
+                if (input.substring(cursor, cursor + entityLength).equals(entity))
+                {
+                    buffer.append(value);
+                    cursor += entityLength;
+                    continue outer;
+                }
+            }
+
+            buffer.append(input.charAt(cursor));
+            cursor++;
+        }
+
+        return buffer.toString().trim();
+    }
+
+    /**
+     *  Returns true if the  map contains the given key (caseless search) and the value
+     *  is "true" (caseless comparison).
+     * 
+     **/
+
+    private boolean checkBoolean(String key, Map map)
+    {
+        String value = findValueCaselessly(key, map);
+
+        if (value == null)
+            return false;
+
+        return value.equalsIgnoreCase("true");
+    }
+
+    /**
+     *  Gets the current location within the file.  This allows the location to be
+     *  created only as needed, and multiple objects on the same line can share
+     *  the same Location instance.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    protected ILocation getCurrentLocation()
+    {
+        if (_currentLocation == null)
+            _currentLocation = new Location(_resourceLocation, _line);
+
+        return _currentLocation;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/TemplateToken.java b/tapestry-framework/src/org/apache/tapestry/parse/TemplateToken.java
new file mode 100644
index 0000000..cc21928
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/TemplateToken.java
@@ -0,0 +1,77 @@
+//  Copyright 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.tapestry.parse;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.tapestry.ILocatable;
+import org.apache.tapestry.ILocation;
+
+/**
+ *  Base class for a number of different types of tokens that can be extracted
+ *  from a page/component template.  This class defines the
+ *  type of the token,
+ *  subclasses provide interpretations on the token.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public abstract class TemplateToken implements ILocatable
+{
+    private TokenType _type;
+    private ILocation _location;
+
+    protected TemplateToken(TokenType type, ILocation location)
+    {
+        _type = type;
+        _location = location;
+    }
+
+    public TokenType getType()
+    {
+        return _type;
+    }
+    
+    public ILocation getLocation()
+    {
+    	return _location;
+    }
+
+    public String toString()
+    {
+        ToStringBuilder builder = new ToStringBuilder(this);
+
+        builder.append("type", _type.getName());
+        builder.append("location", _location);
+
+        extendDescription(builder);
+
+        return builder.toString();
+    }
+
+    /**
+     *  Overridden in subclasses to append additional fields (defined in the subclass)
+     *  to the description.  Subclasses may override this method without invoking
+     *  this implementation, which is empty.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    protected void extendDescription(ToStringBuilder builder)
+    {
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/TextToken.java b/tapestry-framework/src/org/apache/tapestry/parse/TextToken.java
new file mode 100644
index 0000000..4840891
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/TextToken.java
@@ -0,0 +1,183 @@
+//  Copyright 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.tapestry.parse;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Represents static text in the template that may be passed through
+ *  to the client unchanged (except, perhaps, for the removal of
+ *  some whitespace).
+ *
+ *  @see TokenType#TEXT
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class TextToken extends TemplateToken implements IRender
+{
+    private char[] _templateData;
+
+    private int _startIndex = -1;
+    private int _endIndex = -1;
+
+    private int _offset;
+    private int _length;
+    private boolean _needsTrim = true;
+
+    public TextToken(char[] templateData, int startIndex, int endIndex, ILocation location)
+    {
+        super(TokenType.TEXT, location);
+
+        if (startIndex < 0
+            || endIndex < 0
+            || startIndex > templateData.length
+            || endIndex > templateData.length)
+            throw new IllegalArgumentException(
+                Tapestry.format(
+                    "TextToken.range-error",
+                    this,
+                    Integer.toString(templateData.length)));
+
+        _templateData = templateData;
+        _startIndex = startIndex;
+        _endIndex = endIndex;
+
+        // Values actually used to render, may be adjusted to remove excess
+        // leading and trailing whitespace.
+
+        _offset = startIndex;
+        _length = endIndex - startIndex + 1;
+    }
+
+    public synchronized void render(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        if (_needsTrim)
+        {
+            trim();
+            _needsTrim = false;
+        }
+
+        if (_length == 0)
+            return;
+
+        // At one time, we would check to see if the cycle was rewinding and
+        // only invoke printRaw() if it was.  However, that slows down
+        // normal rendering (microscopically) and, with the new
+        // NullResponseWriter class, the "cost" of invoking cycle.isRewinding()
+        // is approximately the same as the "cost" of invoking writer.printRaw().
+
+        writer.printRaw(_templateData, _offset, _length);
+    }
+
+    /**
+      *  Strip off all leading and trailing whitespace by adjusting offset and length.
+      *
+      **/
+
+    private void trim()
+    {
+        if (_length == 0)
+            return;
+
+        try
+        {
+            boolean didTrim = false;
+
+            // Shave characters off the end until we hit a non-whitespace
+            // character.
+
+            while (_length > 0)
+            {
+                char ch = _templateData[_offset + _length - 1];
+
+                if (!Character.isWhitespace(ch))
+                    break;
+
+                _length--;
+                didTrim = true;
+            }
+
+            // Restore one character of whitespace to the end
+
+            if (didTrim)
+                _length++;
+
+            didTrim = false;
+
+            // Strip characters off the front until we hit a non-whitespace
+            // character.
+
+            while (_length > 0)
+            {
+                char ch = _templateData[_offset];
+
+                if (!Character.isWhitespace(ch))
+                    break;
+
+                _offset++;
+                _length--;
+                didTrim = true;
+            }
+
+            // Again, restore one character of whitespace.
+
+            if (didTrim)
+            {
+                _offset--;
+                _length++;
+            }
+
+        }
+        catch (IndexOutOfBoundsException ex)
+        {
+            throw new RuntimeException(Tapestry.format("TextToken.error-trimming", this));
+        }
+
+        // Ok, this isn't perfect.  I don't want to write into templateData[] even
+        // though I'd prefer that my single character of whitespace was always a space.
+        // It would also be kind of neat to shave whitespace within the static HTML, rather
+        // than just on the edges.
+    }
+
+    protected void extendDescription(ToStringBuilder builder)
+    {
+        builder.append("startIndex", _startIndex);
+        builder.append("endIndex", _endIndex);
+    }
+
+    public int getEndIndex()
+    {
+        return _endIndex;
+    }
+
+    public int getStartIndex()
+    {
+        return _startIndex;
+    }
+
+    public char[] getTemplateData()
+    {
+        return _templateData;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/TokenType.java b/tapestry-framework/src/org/apache/tapestry/parse/TokenType.java
new file mode 100644
index 0000000..20c8540
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/TokenType.java
@@ -0,0 +1,74 @@
+//  Copyright 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.tapestry.parse;
+
+import org.apache.commons.lang.enum.Enum;
+
+/**
+ * An {@link Enum} of the different possible token types.
+ *
+ * @see TemplateToken
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * 
+ **/
+
+public class TokenType extends Enum
+{
+    /**
+     *  Raw HTML text.
+     * 
+     *  @see TextToken
+     * 
+     *
+     **/
+
+    public static final TokenType TEXT = new TokenType("TEXT");
+
+    /**
+     *  The opening tag of an element.
+     * 
+     *  @see OpenToken
+     *
+     **/
+
+    public static final TokenType OPEN = new TokenType("OPEN");
+
+    /**
+     *  The closing tag of an element.
+     * 
+     *  @see CloseToken
+     *
+     **/
+
+    public static final TokenType CLOSE = new TokenType("CLOSE");
+
+    /**
+     * 
+     *  A reference to a localized string.
+     * 
+     *  @since 2.0.4
+     * 
+     **/
+    
+    public static final TokenType LOCALIZATION = new TokenType("LOCALIZATION");
+    
+    private TokenType(String name)
+    {
+        super(name);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/ValidatePublicIdRule.java b/tapestry-framework/src/org/apache/tapestry/parse/ValidatePublicIdRule.java
new file mode 100644
index 0000000..254585e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/ValidatePublicIdRule.java
@@ -0,0 +1,76 @@
+//  Copyright 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.tapestry.parse;
+
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.util.xml.DocumentParseException;
+import org.xml.sax.Attributes;
+
+/**
+ *  Rule used to validate the public id of the document, ensuring that
+ *  it is not null, and that it matches an expected value.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class ValidatePublicIdRule extends BaseDocumentRule
+{
+    private String[] _publicIds;
+    private String _rootElement;
+
+    public ValidatePublicIdRule(String[] publicIds, String rootElement)
+    {
+        _publicIds = publicIds;
+        _rootElement = rootElement;
+    }
+
+    public void startDocument(String namespace, String name, Attributes attributes)
+        throws Exception
+    {
+        SpecificationDigester digester = getDigester();
+        IResourceLocation location = digester.getResourceLocation();
+
+        String publicId = digester.getPublicId();
+
+		// publicId will never be null because we use a validating parser.
+
+        for (int i = 0; i < _publicIds.length; i++)
+        {
+            if (_publicIds[i].equals(publicId))
+            {
+
+                if (!name.equals(_rootElement))
+                    throw new DocumentParseException(
+                        Tapestry.format(
+                            "AbstractDocumentParser.incorrect-document-type",
+                            _rootElement,
+                            name),
+                        location);
+
+                return;
+            }
+
+        }
+
+        throw new DocumentParseException(
+            Tapestry.format("AbstractDocumentParser.unknown-public-id", location, publicId),
+            location);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/ValidateRule.java b/tapestry-framework/src/org/apache/tapestry/parse/ValidateRule.java
new file mode 100644
index 0000000..ee07e5a
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/ValidateRule.java
@@ -0,0 +1,68 @@
+//  Copyright 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.tapestry.parse;
+
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.util.RegexpMatcher;
+import org.apache.tapestry.util.xml.InvalidStringException;
+import org.xml.sax.Attributes;
+
+/**
+ *  Validates that an attribute matches a specified pattern.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class ValidateRule extends AbstractSpecificationRule
+{
+    private RegexpMatcher _matcher;
+    private String _attributeName;
+    private String _pattern;
+    private String _errorKey;
+
+	public ValidateRule(RegexpMatcher matcher, String attributeName, String pattern, String errorKey)
+	{
+		_matcher = matcher;
+		_attributeName = attributeName;
+		_pattern = pattern;
+		_errorKey = errorKey;
+	}
+
+	/**
+	 *  Validates that the attribute, if provided, matches the pattern.
+	 * 
+	 *  @throws InvalidStringException if the value does not match the pattern.
+	 * 
+	 **/
+	
+    public void begin(String namespace, String name, Attributes attributes) throws Exception
+    {
+        String value = getValue(attributes, _attributeName);
+        if (value == null)
+            return;
+
+        if (_matcher.matches(_pattern, value))
+            return;
+
+        throw new InvalidStringException(
+            Tapestry.format(_errorKey, value),
+            value,
+            getResourceLocation());
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/parse/package.html b/tapestry-framework/src/org/apache/tapestry/parse/package.html
new file mode 100644
index 0000000..1f6422c
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/parse/package.html
@@ -0,0 +1,14 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+<p>Classes used when parsing templates, application and component specifications.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/record/ChangeKey.java b/tapestry-framework/src/org/apache/tapestry/record/ChangeKey.java
new file mode 100644
index 0000000..39e61b7
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/record/ChangeKey.java
@@ -0,0 +1,92 @@
+//  Copyright 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.tapestry.record;
+
+import org.apache.commons.lang.builder.EqualsBuilder;
+import org.apache.commons.lang.builder.HashCodeBuilder;
+
+/**
+ *  Used to identify a property change.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class ChangeKey
+{
+    private int _hashCode = -1;
+    private String _componentPath;
+    private String _propertyName;
+
+    public ChangeKey(String componentPath, String propertyName)
+    {
+        _componentPath = componentPath;
+        _propertyName = propertyName;
+    }
+
+    public boolean equals(Object object)
+    {
+        if (object == null)
+            return false;
+
+        if (this == object)
+            return true;
+
+        if (!(object instanceof ChangeKey))
+            return false;
+
+        ChangeKey other = (ChangeKey) object;
+
+        EqualsBuilder builder = new EqualsBuilder();
+
+        builder.append(_propertyName, other._propertyName);
+        builder.append(_componentPath, other._componentPath);
+
+        return builder.isEquals();
+    }
+
+    public String getComponentPath()
+    {
+        return _componentPath;
+    }
+
+    public String getPropertyName()
+    {
+        return _propertyName;
+    }
+
+    /**
+     *
+     *  Returns a hash code computed from the
+     *  property name and component path.
+     *
+     **/
+
+    public int hashCode()
+    {
+        if (_hashCode == -1)
+        {
+            HashCodeBuilder builder = new HashCodeBuilder(257, 23); // Random
+
+            builder.append(_propertyName);
+            builder.append(_componentPath);
+
+            _hashCode = builder.toHashCode();
+        }
+
+        return _hashCode;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/record/IPageChange.java b/tapestry-framework/src/org/apache/tapestry/record/IPageChange.java
new file mode 100644
index 0000000..7866125
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/record/IPageChange.java
@@ -0,0 +1,48 @@
+//  Copyright 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.tapestry.record;
+
+/**
+ *  Represents a change to a component on a page, this represents
+ *  a datum of information stored by a {@link org.apache.tapestry.engine.IPageRecorder}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ **/
+
+public interface IPageChange
+{
+    /**
+     *  The path to the component on the page, or null if the property is a property
+     *  of the page.
+     *
+     **/
+
+    public String getComponentPath();
+
+    /**
+     *  The new value for the property, which may be null.
+     *
+     **/
+
+    public Object getNewValue();
+
+    /**
+     *  The name of the property that changed.
+     *
+     **/
+
+    public String getPropertyName();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/record/PageChange.java b/tapestry-framework/src/org/apache/tapestry/record/PageChange.java
new file mode 100644
index 0000000..fe196b0
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/record/PageChange.java
@@ -0,0 +1,82 @@
+//  Copyright 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.tapestry.record;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+
+/**
+ *  Represents a change to a component on a page.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class PageChange implements IPageChange
+{
+    private String _componentPath;
+    private String _propertyName;
+    private Object _newValue;
+
+    public PageChange(String componentPath, String propertyName, Object newValue)
+    {
+        _componentPath = componentPath;
+        _propertyName = propertyName;
+        _newValue = newValue;
+    }
+
+    /**
+     *  The path to the component on the page, or null if the property
+     *  is a property of the page.
+     *
+     **/
+
+    public String getComponentPath()
+    {
+        return _componentPath;
+    }
+
+    /**
+     *  The new value for the property, which may be null.
+     *
+     **/
+
+    public Object getNewValue()
+    {
+        return _newValue;
+    }
+
+    /**
+     *  The name of the property that changed.
+     *
+     **/
+
+    public String getPropertyName()
+    {
+        return _propertyName;
+    }
+
+    public String toString()
+    {
+        ToStringBuilder builder = new ToStringBuilder(this);
+        
+        builder.append("componentPath", _componentPath);
+        builder.append("propertyName", _propertyName);
+        builder.append("newValue", _newValue);
+        
+        return builder.toString();
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/record/PageRecorder.java b/tapestry-framework/src/org/apache/tapestry/record/PageRecorder.java
new file mode 100644
index 0000000..ad03155
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/record/PageRecorder.java
@@ -0,0 +1,238 @@
+//  Copyright 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.tapestry.record;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.IPage;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.engine.IPageRecorder;
+import org.apache.tapestry.event.ObservedChangeEvent;
+import org.apache.tapestry.util.prop.OgnlUtils;
+
+/**
+ *  Tracks changes to components on a page, allowing changes to be persisted across
+ *  request cycles, and restoring the state of a page and component when needed.
+ *
+ *  <p>This is an abstract implementation; specific implementations can choose where
+ *  and how to persist the data.
+ *
+ * @author Howard Ship
+ * @version $Id$
+ * 
+ **/
+
+public abstract class PageRecorder implements IPageRecorder
+{
+    private boolean _dirty = false;
+    private boolean _locked = false;
+    private boolean _discard = false;
+
+    /**
+     *  Invoked to persist all changes that have been accumulated.  If the recorder
+     *  saves change incrementally, this should ensure that all changes have been persisted.
+     *
+     *  <p>Subclasses should check the dirty flag.  If the recorder is dirty, changes
+     *  should be recorded and the dirty flag cleared.
+     *
+     **/
+
+    public abstract void commit();
+
+    /**
+     *  Returns a <code>Collection</code> of 
+     *  {@link IPageChange} objects
+     *  identifying changes to the page and its components.
+     *
+     **/
+
+    public abstract Collection getChanges();
+
+    /**
+     *  Returns true if the page has observed a change.
+     *  The dirty flag is cleared by
+     *  {@link #commit()}.
+     *
+     **/
+
+    public boolean isDirty()
+    {
+        return _dirty;
+    }
+
+    /**
+     *  Returns true if the recorder is locked.  The locked flag
+     *  is set by {@link #commit()}.
+     *
+     **/
+
+    public boolean isLocked()
+    {
+        return _locked;
+    }
+
+    public void setLocked(boolean value)
+    {
+        _locked = value;
+    }
+
+    /**
+     *  Observes the change.  The object of the event is expected to
+     *  be an {@link IComponent}.  Ignores the change if not active,
+     *  otherwise, sets invokes {@link #recordChange(String, String,
+     *  Object)}.
+     *
+     *  <p>If the property name in the event is null, then the recorder
+     *  is marked dirty (but 
+     *  {@link #recordChange(String, String,
+     *  Object)} is not invoked.  This is how a "distant" property changes
+     *  are propogated to the page recorder (a distant property change is a change to
+     *  a property of an object that is itself a property of the page).
+     *
+     *  <p>If the recorder is not active (typically, when a page is
+     *  being rewound), then the event is simply ignored.
+     *
+     **/
+
+    public void observeChange(ObservedChangeEvent event)
+    {
+        IComponent component = event.getComponent();
+        String propertyName = event.getPropertyName();
+
+        if (_locked)
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "PageRecorder.change-after-lock",
+                    component.getPage().getPageName(),
+                    propertyName,
+                    component.getExtendedId()));
+
+        if (propertyName == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.format("PageRecorder.null-property-name", component.getExtendedId()));
+
+        Object activeValue = event.getNewValue();
+
+        try
+        {
+            recordChange(component.getIdPath(), propertyName, activeValue);
+        }
+        catch (Throwable t)
+        {
+            t.printStackTrace();
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "PageRecorder.unable-to-persist",
+                    propertyName,
+                    component.getExtendedId(),
+                    activeValue),
+                t);
+        }
+    }
+
+    /**
+     *  Records a change to a particular component.  Subclasses may
+     *  cache these in memory, or record them externally at this time.
+     *
+     *  <p>This method is responsible for setting the dirty flag if
+     *  the described change is real.
+     *
+     *  @param componentPath the name of the component relative to the
+     *  page which contains it.  May be null if the change was to a
+     *  property of the page itself.
+     *
+     *  @param propertyName the name of the property which changed.
+     *
+     *  @param newValue the new value for the property, which may also
+     *  be null.
+     *
+     *  @see IComponent#getIdPath()
+     *
+     **/
+
+    protected abstract void recordChange(
+        String componentPath,
+        String propertyName,
+        Object newValue);
+
+    /**
+     *  Rolls back the page to the currently persisted state.
+     *
+     **/
+
+    public void rollback(IPage page)
+    {
+        Collection changes = getChanges();
+
+        if (changes.isEmpty())
+            return;
+
+        IResourceResolver resolver = page.getEngine().getResourceResolver();
+        Iterator i = changes.iterator();
+
+        while (i.hasNext())
+        {
+            PageChange change = (PageChange) i.next();
+
+            String propertyName = change.getPropertyName();
+
+            IComponent component = page.getNestedComponent(change.getComponentPath());
+
+            Object storedValue = change.getNewValue();
+
+            try
+            {
+                OgnlUtils.set(propertyName, resolver, component, storedValue);
+            }
+            catch (Throwable t)
+            {
+                throw new ApplicationRuntimeException(
+                    Tapestry.format(
+                        "PageRecorder.unable-to-rollback",
+                        new Object[] { propertyName, component, storedValue, t.getMessage()}),
+                    t);
+            }
+        }
+    }
+
+    /** @since 2.0.2 **/
+
+    public boolean isMarkedForDiscard()
+    {
+        return _discard;
+    }
+
+    /** @since 2.0.2 **/
+
+    public void markForDiscard()
+    {
+        _discard = true;
+    }
+
+    protected void setDirty(boolean dirty)
+    {
+        _dirty = dirty;
+    }
+
+    protected boolean getDirty()
+    {
+        return _dirty;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/record/SessionPageRecorder.java b/tapestry-framework/src/org/apache/tapestry/record/SessionPageRecorder.java
new file mode 100644
index 0000000..a3fc705
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/record/SessionPageRecorder.java
@@ -0,0 +1,248 @@
+//  Copyright 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.tapestry.record;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.servlet.http.HttpSession;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.request.RequestContext;
+import org.apache.tapestry.util.StringSplitter;
+
+/**
+ * Simple implementation of {@link org.apache.tapestry.engine.IPageRecorder}that stores page
+ * changes as {@link javax.servlet.http.HttpSession}attributes.
+ * 
+ * @author Howard Ship
+ * @version $Id$
+ */
+
+public class SessionPageRecorder extends PageRecorder
+{
+    private static final Log LOG = LogFactory.getLog(SessionPageRecorder.class);
+
+    /**
+     * Dictionary of changes, keyed on an instance of {@link ChangeKey}(which enapsulates component
+     * path and property name). The value is the new value for the object. The same information is
+     * stored into the {@link HttpSession}, which is used as a kind of write-behind cache.
+     */
+
+    private Map _changes;
+
+    /**
+     * The session into which changes are recorded.
+     * 
+     * @since 3.0
+     */
+
+    private HttpSession _session;
+
+    /**
+     * The fully qualified name of the page being recorded.
+     * 
+     * @since 3.0
+     */
+
+    private String _pageName;
+
+    /**
+     * The prefix (for {@link HttpSession}attributes) used by this page recorder.
+     */
+
+    private String _attributePrefix;
+
+    public void initialize(String pageName, IRequestCycle cycle)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Initializing for " + pageName);
+
+        RequestContext context = cycle.getRequestContext();
+
+        _pageName = pageName;
+        _session = context.getSession();
+
+        _attributePrefix = context.getServlet().getServletName() + "/" + _pageName + "/";
+
+        restorePageChanges();
+    }
+
+    public void discard()
+    {
+        if (Tapestry.isEmpty(_changes))
+            return;
+
+        Iterator i = _changes.keySet().iterator();
+
+        while (i.hasNext())
+        {
+            ChangeKey key = (ChangeKey) i.next();
+
+            String attributeKey = constructAttributeKey(key.getComponentPath(), key
+                    .getPropertyName());
+
+            if (LOG.isDebugEnabled())
+                LOG.debug("Removing session attribute " + attributeKey);
+
+            _session.removeAttribute(attributeKey);
+        }
+    }
+
+    /**
+     * Simply clears the dirty flag, because there is no external place to store changed page
+     * properties. Sets the locked flag to prevent subsequent changes from occuring now.
+     */
+
+    public void commit()
+    {
+        setDirty(false);
+        setLocked(true);
+    }
+
+    /**
+     * Returns true if the recorder has any changes recorded.
+     */
+
+    public boolean getHasChanges()
+    {
+        if (_changes == null)
+            return false;
+
+        return (_changes.size() > 0);
+    }
+
+    public Collection getChanges()
+    {
+        if (_changes == null)
+            return Collections.EMPTY_LIST;
+
+        int count = _changes.size();
+        Collection result = new ArrayList(count);
+
+        Iterator i = _changes.entrySet().iterator();
+        while (i.hasNext())
+        {
+            Map.Entry entry = (Map.Entry) i.next();
+
+            ChangeKey key = (ChangeKey) entry.getKey();
+
+            Object value = entry.getValue();
+
+            PageChange change = new PageChange(key.getComponentPath(), key.getPropertyName(), value);
+
+            result.add(change);
+        }
+
+        return result;
+    }
+
+    protected void recordChange(String componentPath, String propertyName, Object newValue)
+    {
+        ChangeKey key = new ChangeKey(componentPath, propertyName);
+
+        if (_changes == null)
+            _changes = new HashMap();
+
+        setDirty(true);
+
+        _changes.put(key, newValue);
+
+        // Now, build a key used to store the new value
+        // in the HttpSession
+
+        String attributeKey = constructAttributeKey(componentPath, propertyName);
+
+        if (newValue == null)
+            _session.removeAttribute(attributeKey);
+        else
+            _session.setAttribute(attributeKey, newValue);
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Stored session attribute " + attributeKey + " = " + newValue);
+    }
+
+    private String constructAttributeKey(String componentPath, String propertyName)
+    {
+        StringBuffer buffer = new StringBuffer(_attributePrefix);
+
+        if (componentPath != null)
+        {
+            buffer.append(componentPath);
+            buffer.append('/');
+        }
+
+        buffer.append(propertyName);
+
+        return buffer.toString();
+    }
+
+    private void restorePageChanges()
+    {
+        int count = 0;
+        Enumeration e = _session.getAttributeNames();
+        StringSplitter splitter = null;
+
+        while (e.hasMoreElements())
+        {
+            String key = (String) e.nextElement();
+
+            if (!key.startsWith(_attributePrefix))
+                continue;
+
+            if (LOG.isDebugEnabled())
+                LOG.debug("Restoring page change from session attribute " + key);
+
+            if (_changes == null)
+            {
+                _changes = new HashMap();
+
+                splitter = new StringSplitter('/');
+            }
+
+            String[] names = splitter.splitToArray(key);
+
+            // The first name is the servlet name, which allows
+            // multiple Tapestry apps to share a HttpSession, even
+            // when they use the same page names. The second name
+            // is the page name, which we already know.
+
+            int i = 2;
+
+            String componentPath = (names.length == 4) ? names[i++] : null;
+            String propertyName = names[i++];
+            Object value = _session.getAttribute(key);
+
+            ChangeKey changeKey = new ChangeKey(componentPath, propertyName);
+
+            _changes.put(changeKey, value);
+
+            count++;
+        }
+
+        if (LOG.isDebugEnabled())
+            LOG.debug(count == 0 ? "No recorded changes." : "Restored " + count
+                    + " recorded changes.");
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/record/package.html b/tapestry-framework/src/org/apache/tapestry/record/package.html
new file mode 100644
index 0000000..54a0a95
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/record/package.html
@@ -0,0 +1,15 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+<p>Abstract and simple (memory-based) implementations of
+{@link org.apache.tapestry.engine.IPageRecorder}.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/request/DecodedRequest.java b/tapestry-framework/src/org/apache/tapestry/request/DecodedRequest.java
new file mode 100644
index 0000000..5c2101b
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/request/DecodedRequest.java
@@ -0,0 +1,85 @@
+//  Copyright 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.tapestry.request;
+
+/**
+ *  Contains properties of an {@link javax.servlet.http.HttpServletRequest}
+ *  that have been extracted from the request (or otherwise determined).
+ * 
+ *  <p>An alternative idea would have been to create a new 
+ *  {@link javax.servlet.http.HttpServletRequest}
+ *  wrapper that overode the various methods.  That struck me as causing
+ *  more confusion; instead (in the few places it counts), classes will
+ *  get the decoded properties from the {@link RequestContext}.
+ *
+ *  @see IRequestDecoder
+ *  @see RequestContext#getScheme()
+ *  @see RequestContext#getServerName()
+ *  @see RequestContext#getServerPort()
+ *  @see RequestContext#getRequestURI()
+ * 
+ *  @author Howard Lewis Ship
+ *  @version DecodedRequest.java,v 1.1 2002/08/20 21:49:58 hship Exp
+ *  @since 2.2
+ * 
+ **/
+
+public class DecodedRequest
+{
+    private String _scheme;
+    private String _serverName;
+    private String _requestURI;
+    private int _serverPort;
+
+    public int getServerPort()
+    {
+        return _serverPort;
+    }
+
+    public String getScheme()
+    {
+        return _scheme;
+    }
+
+    public String getServerName()
+    {
+        return _serverName;
+    }
+
+    public String getRequestURI()
+    {
+        return _requestURI;
+    }
+
+    public void setServerPort(int serverPort)
+    {
+        _serverPort = serverPort;
+    }
+
+    public void setScheme(String scheme)
+    {
+        _scheme = scheme;
+    }
+
+    public void setServerName(String serverName)
+    {
+        _serverName = serverName;
+    }
+
+    public void setRequestURI(String URI)
+    {
+        _requestURI = URI;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/request/IRequestDecoder.java b/tapestry-framework/src/org/apache/tapestry/request/IRequestDecoder.java
new file mode 100644
index 0000000..5743019
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/request/IRequestDecoder.java
@@ -0,0 +1,44 @@
+//  Copyright 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.tapestry.request;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ *  Given a {@link javax.servlet.http.HttpServletRequest}, identifies
+ *  the correct request properties (server, scheme, URI and port).
+ * 
+ *  <p>An implementation of this class may be necessary when using
+ *  Tapestry with specific firewalls which may obscure
+ *  the scheme, server, etc. visible to the client web browser
+ *  (the request appears to arrive from the firewall server, not the
+ *  client web browser).
+ *
+ *  @author Howard Lewis Ship
+ *  @version IRequestDecoder.java,v 1.1 2002/08/20 21:49:58 hship Exp
+ *  @since 2.2
+ * 
+ **/
+
+public interface IRequestDecoder
+{
+
+    /**
+     *  Invoked to identify the actual properties from the request.
+     * 
+     **/
+
+    public DecodedRequest decodeRequest(HttpServletRequest request);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/request/IUploadFile.java b/tapestry-framework/src/org/apache/tapestry/request/IUploadFile.java
new file mode 100644
index 0000000..951271a
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/request/IUploadFile.java
@@ -0,0 +1,99 @@
+//  Copyright 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.tapestry.request;
+
+import java.io.File;
+import java.io.InputStream;
+
+/**
+ *  Represents a file uploaded from a client side form.
+ * 
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.8
+ *
+ **/
+
+public interface IUploadFile
+{
+	/**
+	 *  Returns the name of the file that was uploaded.  This
+	 *  is just the filename portion of the complete path.
+	 * 
+	 **/
+
+	public String getFileName();
+
+	/**
+	 *  Returns the complete path, as reported by the client
+	 *  browser.  Different browsers report different things
+	 *  here.
+	 * 
+	 * 
+	 *  @since 2.0.4
+	 * 
+	 **/
+	
+	public String getFilePath();
+
+	/**
+	 *  Returns an input stream of the content of the file.  There is no guarantee
+	 *  that this stream will be valid after the end of the current request cycle,
+	 *  so it should be processed immediately.
+	 * 
+	 *  <p>As of release 1.0.8, this will be a a {@link java.io.ByteArrayInputStream},
+	 *  but that, too, may change (a future implementation may upload the stream
+	 *  to a temporary file and return an input stream from that).
+	 * 
+	 **/
+
+	public InputStream getStream();
+    
+    /**
+     *  Returns the MIME type specified when the file was uploaded.  May return null
+     *  if the content type is not known.
+     * 
+     *  @since 2.2
+     * 
+     **/
+    
+    public String getContentType();
+    
+    /**
+     * Writes the content of the file to a known location.  This should
+     * be invoked at most once.  In a standard
+     * implementation based on Jakarta FileUpload, this will often
+     * be implemented efficiently as a file rename.
+     * 
+     * @since 3.0
+     */
+    
+    public void write(File file);
+    
+    /**
+     * Returns true if the uploaded content is in memory.  False generally
+     * means the content is stored in a temporary file.
+     */
+    
+    public boolean isInMemory();
+    
+    /**
+     * Returns the size, in bytes, of the uploaded content.
+     * 
+     * @since 3.0
+     **/
+    
+    public long getSize(); 
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/request/RequestContext.java b/tapestry-framework/src/org/apache/tapestry/request/RequestContext.java
new file mode 100644
index 0000000..edffc0c
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/request/RequestContext.java
@@ -0,0 +1,1096 @@
+//  Copyright 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.tapestry.request;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.StringTokenizer;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ApplicationServlet;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.multipart.DefaultMultipartDecoder;
+import org.apache.tapestry.multipart.IMultipartDecoder;
+import org.apache.tapestry.spec.IApplicationSpecification;
+import org.apache.tapestry.util.IRenderDescription;
+
+/**
+ *  This class encapsulates all the relevant data for one request cycle of an
+ *  {@link ApplicationServlet}.  This includes:
+ *  <ul>
+ *  	<li>{@link HttpServletRequest}
+ *		<li>{@link HttpServletResponse}
+ *		<li>{@link HttpSession}
+ * 		<li>{@link javax.servlet.http.HttpServlet}
+ *  </ul>
+ *  <p>It also provides methods for:
+ *  <ul>
+ *  <li>Retrieving the request parameters (even if a file upload is involved)
+ *  <li>Getting, setting and removing request attributes
+ *  <li>Forwarding requests
+ *  <li>Redirecting requests
+ *  <li>Getting and setting Cookies
+ *  <li>Intepreting the request path info
+ *  <li>Writing an HTML description of the <code>RequestContext</code> (for debugging).
+ *  </ul>
+ *
+ * 
+ *  <p>
+ *  If some cases, it is necesary to provide an implementation of
+ *  {@link IRequestDecoder} (often, due to a firewall).
+ *  If the application specifification
+ *  provides an extension named
+ *  <code>org.apache.tapestry.request-decoder</code>
+ *  then it will be used, instead of a default decoder.
+ * 
+ *  <p>This class is not a component, but does implement {@link IRender}.  When asked to render
+ *  (perhaps as the delegate of a {@link org.apache.tapestry.components.Delegator} component}
+ *  it simply invokes {@link #write(IMarkupWriter)} to display all debugging output.
+ *
+ *  <p>This class is derived from the original class 
+ *  <code>com.primix.servlet.RequestContext</code>,
+ *  part of the <b>ServletUtils</b> framework available from
+ *  <a href="http://www.gjt.org/servlets/JCVSlet/list/gjt/com/primix/servlet">The Giant 
+ *  Java Tree</a>.
+ *
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ * 
+ **/
+
+public class RequestContext implements IRender
+{
+    /** @since 2.2 **/
+
+    private static class DefaultRequestDecoder implements IRequestDecoder
+    {
+        public DecodedRequest decodeRequest(HttpServletRequest request)
+        {
+            DecodedRequest result = new DecodedRequest();
+
+            result.setRequestURI(request.getRequestURI());
+            result.setScheme(request.getScheme());
+            result.setServerName(request.getServerName());
+            result.setServerPort(request.getServerPort());
+
+            return result;
+        }
+    }
+
+    private static final Log LOG = LogFactory.getLog(RequestContext.class);
+
+    private HttpSession _session;
+    private HttpServletRequest _request;
+    private HttpServletResponse _response;
+    private ApplicationServlet _servlet;
+    private DecodedRequest _decodedRequest;
+    private IMultipartDecoder _decoder;
+    private boolean _decoded;
+
+    /**
+     * A mapping of the cookies available in the request.
+     *
+     **/
+
+    private Map _cookieMap;
+
+    /**
+     *  Used during {@link #write(IMarkupWriter)}.
+     * 
+     **/
+
+    private boolean _evenRow;
+
+    /**
+     * Creates a <code>RequestContext</code> from its components.
+     *
+     **/
+
+    public RequestContext(
+        ApplicationServlet servlet,
+        HttpServletRequest request,
+        HttpServletResponse response)
+        throws IOException
+    {
+        _servlet = servlet;
+        _request = request;
+        _response = response;
+
+        // All three parameters may be null if created from
+        // AbstractEngine.cleanupEngine().
+
+        if (_request != null && DefaultMultipartDecoder.isMultipartRequest(request))
+        {
+            IMultipartDecoder decoder = obtainMultipartDecoder(servlet, request);
+            setDecoder(decoder);
+        }
+    }
+
+    /**
+     *  Invoked from the constructor to create a {@link DefaultMultipartDecoder} instance.
+     *  Applications with specific upload needs may need to override this to
+     *  provide a subclass instance instead.  The caller will invoke
+     *  {@link IMultipartDecoder#decode(HttpServletRequest)} on the
+     *  returned object.
+     * 
+     *  <p>
+     *  This implementation checks for application extension
+     *  {@link Tapestry#MULTIPART_DECODER_EXTENSION_NAME}.  If that is not
+     *  defined, a shared instance of {@link DefaultMultipartDecoder}
+     *  is returned.  
+     *
+     * 
+     *  @see ApplicationServlet#createRequestContext(HttpServletRequest, HttpServletResponse)
+     *  @since 3.0
+     * 
+     **/
+
+    protected IMultipartDecoder obtainMultipartDecoder(
+        ApplicationServlet servlet,
+        HttpServletRequest request)
+        throws IOException
+    {
+        IApplicationSpecification spec = servlet.getApplicationSpecification();
+
+        if (spec.checkExtension(Tapestry.MULTIPART_DECODER_EXTENSION_NAME))
+            return (IMultipartDecoder) spec.getExtension(
+                Tapestry.MULTIPART_DECODER_EXTENSION_NAME,
+                IMultipartDecoder.class);
+
+        return DefaultMultipartDecoder.getSharedInstance();
+    }
+
+    /**
+     * Adds a simple {@link Cookie}. To set a Cookie with attributes,
+     * use {@link #addCookie(Cookie)}.
+     *
+     **/
+
+    public void addCookie(String name, String value)
+    {
+        addCookie(new Cookie(name, value));
+    }
+
+    /**
+     * Adds a {@link Cookie} to the response. Once added, the
+     * Cookie will also be available to {@link #getCookie(String)} method.
+     *
+     * <p>Cookies should only be added <em>before</em> invoking
+     * {@link HttpServletResponse#getWriter()}..
+     *
+     **/
+
+    public void addCookie(Cookie cookie)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Adding cookie " + cookie);
+
+        _response.addCookie(cookie);
+
+        if (_cookieMap == null)
+            readCookieMap();
+
+        _cookieMap.put(cookie.getName(), cookie);
+    }
+
+    private void datePair(IMarkupWriter writer, String name, long value)
+    {
+        pair(writer, name, new Date(value));
+    }
+
+    /** @since 2.2 **/
+
+    private DecodedRequest getDecodedRequest()
+    {
+        if (_decodedRequest != null)
+            return _decodedRequest;
+
+        IApplicationSpecification spec = _servlet.getApplicationSpecification();
+        IRequestDecoder decoder = null;
+
+        if (!spec.checkExtension(Tapestry.REQUEST_DECODER_EXTENSION_NAME))
+            decoder = new DefaultRequestDecoder();
+        else
+            decoder =
+                (IRequestDecoder) spec.getExtension(
+                    Tapestry.REQUEST_DECODER_EXTENSION_NAME,
+                    IRequestDecoder.class);
+
+        _decodedRequest = decoder.decodeRequest(_request);
+
+        return _decodedRequest;
+    }
+
+    /** 
+     * 
+     *  Returns the actual scheme, possibly decoded from the request.
+     * 
+     *  @see IRequestDecoder
+     *  @see javax.servlet.ServletRequest#getScheme()
+     *  @since 2.2  
+     * 
+     **/
+
+    public String getScheme()
+    {
+        return getDecodedRequest().getScheme();
+    }
+
+    /** 
+     * 
+     *  Returns the actual server name, possibly decoded from the request.
+     * 
+     *  @see IRequestDecoder
+     *  @see javax.servlet.ServletRequest#getServerName()
+     *  @since 2.2  
+     * 
+     **/
+
+    public String getServerName()
+    {
+        return getDecodedRequest().getServerName();
+    }
+
+    /** 
+     * 
+     *  Returns the actual server port, possibly decoded from the request.
+     * 
+     *  @see IRequestDecoder
+     *  @see javax.servlet.ServletRequest#getServerPort()
+     *  @since 2.2  
+     * 
+     **/
+
+    public int getServerPort()
+    {
+        return getDecodedRequest().getServerPort();
+    }
+
+    /** 
+     * 
+     *  Returns the actual request URI, possibly decoded from the request.
+     * 
+     *  @see IRequestDecoder
+     *  @see HttpServletRequest#getRequestURI()
+     *  @since 2.2  
+     * 
+     **/
+
+    public String getRequestURI()
+    {
+        return getDecodedRequest().getRequestURI();
+    }
+
+    /**
+     * Builds an absolute URL from the given URI, using the {@link HttpServletRequest}
+     * as the source for scheme, server name and port.
+     *
+     * @see #getAbsoluteURL(String, String, String, int)
+     * 
+     **/
+
+    public String getAbsoluteURL(String URI)
+    {
+        String scheme = getScheme();
+        String server = getServerName();
+        int port = getServerPort();
+
+        // Keep things simple ... port 80 is accepted as the
+        // standard port for http so it can be ommitted.
+        // Some of the Tomcat code indicates that port 443 is the default
+        // for https, and that needs to be researched.
+
+        if (scheme.equals("http") && port == 80)
+            port = 0;
+
+        return getAbsoluteURL(URI, scheme, server, port);
+    }
+
+    /**
+     * Does some easy checks to turn a path (or URI) into an absolute URL. We assume
+     * <ul>
+     * <li>The presense of a colon means the path is complete already (any other colons
+     * in the URI portion should have been converted to %3A).
+     *
+     * <li>A leading pair of forward slashes means the path is simply missing
+     * the scheme.
+     * <li>Otherwise, we assemble the scheme, server, port (if non-zero) and the URI
+     * as given.
+     * </ul>
+     *
+     **/
+
+    public String getAbsoluteURL(String URI, String scheme, String server, int port)
+    {
+        StringBuffer buffer = new StringBuffer();
+
+        // Though, really, what does a leading colon with no scheme before it
+        // mean?
+
+        if (URI.indexOf(':') >= 0)
+            return URI;
+
+        // Should check the length here, first.
+
+        if (URI.substring(0, 1).equals("//"))
+        {
+            buffer.append(scheme);
+            buffer.append(':');
+            buffer.append(URI);
+            return buffer.toString();
+        }
+
+        buffer.append(scheme);
+        buffer.append("://");
+        buffer.append(server);
+
+        if (port > 0)
+        {
+            buffer.append(':');
+            buffer.append(port);
+        }
+
+        if (URI.charAt(0) != '/')
+            buffer.append('/');
+
+        buffer.append(URI);
+
+        return buffer.toString();
+    }
+
+    /**
+     * Gets a named {@link Cookie}.
+     *
+     * @param name The name of the Cookie.
+     * @return The Cookie, or null if no Cookie with that
+     * name exists.
+     *
+     **/
+
+    public Cookie getCookie(String name)
+    {
+        if (_cookieMap == null)
+            readCookieMap();
+
+        return (Cookie) _cookieMap.get(name);
+    }
+
+    /**
+     * Reads the named {@link Cookie} and returns its value (if it exists), or
+     * null if it does not exist.
+     **/
+
+    public String getCookieValue(String name)
+    {
+        Cookie cookie;
+
+        cookie = getCookie(name);
+
+        if (cookie == null)
+            return null;
+
+        return cookie.getValue();
+    }
+
+    /**
+     *  Returns the named parameter from the {@link HttpServletRequest}.
+     *
+     *  <p>Use {@link #getParameters(String)} for parameters that may
+     *  include multiple values.
+     * 
+     *  <p>This is the preferred way to obtain parameter values (rather than
+     *  obtaining the {@link HttpServletRequest} itself).  For form/multipart-data
+     *  encoded requests, this method will still work.
+     *
+     **/
+
+    public String getParameter(String name)
+    {
+        IMultipartDecoder decoder = getDecoder();
+        if (decoder != null)
+            return decoder.getString(_request, name);
+
+        return _request.getParameter(name);
+    }
+
+    /**
+     *  Convienience method for getting a {@link HttpServletRequest} attribute.
+     * 
+     *  @since 2.3
+     * 
+     **/
+
+    public Object getAttribute(String name)
+    {
+        return _request.getAttribute(name);
+    }
+
+    /**
+     * For parameters that are, or are possibly, multi-valued, this
+     * method returns all the values as an array of Strings.
+     * 
+     *  @see #getParameter(String)
+     *
+     **/
+
+    public String[] getParameters(String name)
+    {
+        // Note: this may not be quite how we want it to work; we'll have to see.
+
+        IMultipartDecoder decoder = getDecoder();
+        if (decoder != null)
+            return decoder.getStrings(_request, name);
+
+        return _request.getParameterValues(name);
+    }
+
+    /**
+     * Returns the named {@link IUploadFile}, if it exists, or null if it doesn't.
+     * Uploads require an encoding of <code>multipart/form-data</code>
+     * (this is specified in the
+     * form's enctype attribute).  If the encoding type
+     * is not so, or if no upload matches the name, then this method returns null.
+     * 
+     **/
+
+    public IUploadFile getUploadFile(String name)
+    {
+        IMultipartDecoder decoder = getDecoder();
+        if (decoder == null)
+            return null;
+
+        return decoder.getUploadFile(_request, name);
+    }
+
+    /**
+     *  Invoked at the end of the request cycle to cleanup and temporary resources.
+     *  This is chained to the {@link DefaultMultipartDecoder}, if there is one.
+     * 
+     *  @since 2.0.1
+     **/
+
+    public void cleanup()
+    {
+        if (_decoder != null)
+            _decoder.cleanup(_request);
+    }
+
+    /**
+     *  Returns the request which initiated the current request cycle.  Note that
+     *  the methods {@link #getParameter(String)} and {@link #getParameters(String)}
+     *  should be used, rather than obtaining parameters directly from the request
+     *  (since the RequestContext handles the differences between normal and multipart/form
+     *  requests).
+     * 
+     **/
+
+    public HttpServletRequest getRequest()
+    {
+        return _request;
+    }
+
+    public HttpServletResponse getResponse()
+    {
+        return _response;
+    }
+
+    private String getRowClass()
+    {
+        String result;
+
+        result = _evenRow ? "even" : "odd";
+
+        _evenRow = !_evenRow;
+
+        return result;
+    }
+
+    public ApplicationServlet getServlet()
+    {
+        return _servlet;
+    }
+
+    /**
+     *  Returns the {@link HttpSession}, if necessary, invoking
+     * {@link HttpServletRequest#getSession(boolean)}.  However,
+     * this method will <em>not</em> create a session.
+     *
+     **/
+
+    public HttpSession getSession()
+    {
+        if (_session == null)
+            _session = _request.getSession(false);
+
+        return _session;
+    }
+
+    /**
+     *  Like {@link #getSession()}, but forces the creation of
+     *  the {@link HttpSession}, if necessary.
+     *
+     **/
+
+    public HttpSession createSession()
+    {
+        if (_session == null)
+        {
+            if (LOG.isDebugEnabled())
+                LOG.debug("Creating HttpSession");
+
+            _session = _request.getSession(true);
+        }
+
+        return _session;
+    }
+
+    private void header(IMarkupWriter writer, String valueName, String dataName)
+    {
+        writer.begin("tr");
+        writer.attribute("class", "request-context-header");
+
+        writer.begin("th");
+        writer.print(valueName);
+        writer.end();
+
+        writer.begin("th");
+        writer.print(dataName);
+        writer.end("tr");
+
+        _evenRow = true;
+    }
+
+    private void object(IMarkupWriter writer, String objectName)
+    {
+        writer.begin("span");
+        writer.attribute("class", "request-context-object");
+        writer.print(objectName);
+        writer.end();
+    }
+
+    private void pair(IMarkupWriter writer, String name, int value)
+    {
+        pair(writer, name, Integer.toString(value));
+    }
+
+    private void pair(IMarkupWriter writer, String name, Object value)
+    {
+        if (value == null)
+            return;
+
+        if (value instanceof IRenderDescription)
+        {
+            IRenderDescription renderValue = (IRenderDescription) value;
+
+            writer.begin("tr");
+            writer.attribute("class", getRowClass());
+
+            writer.begin("th");
+            writer.print(name);
+            writer.end();
+
+            writer.begin("td");
+
+            renderValue.renderDescription(writer);
+
+            writer.end("tr");
+            writer.println();
+            return;
+        }
+
+        pair(writer, name, value.toString());
+    }
+
+    private void pair(IMarkupWriter writer, String name, String value)
+    {
+        if (value == null)
+            return;
+
+        if (value.length() == 0)
+            return;
+
+        writer.begin("tr");
+        writer.attribute("class", getRowClass());
+
+        writer.begin("th");
+        writer.print(name);
+        writer.end();
+
+        writer.begin("td");
+        writer.print(value);
+        writer.end("tr");
+        writer.println();
+    }
+
+    private void pair(IMarkupWriter writer, String name, boolean value)
+    {
+        pair(writer, name, value ? "yes" : "no");
+    }
+
+    private void readCookieMap()
+    {
+        _cookieMap = new HashMap();
+
+        Cookie[] cookies = _request.getCookies();
+
+        if (cookies != null)
+            for (int i = 0; i < cookies.length; i++)
+                _cookieMap.put(cookies[i].getName(), cookies[i]);
+    }
+
+    /**
+     *  Invokes {@link HttpServletResponse#sendRedirect(String)}</code>, 
+     *  but massages <code>path</code>, supplying missing elements to
+     *  make it an absolute URL (i.e., specifying scheme, server, port, etc.).
+     *
+     *  <p>The 2.2 Servlet API will do this automatically, and a little more,
+     *  according to the early documentation.
+     *
+     **/
+
+    public void redirect(String path) throws IOException
+    {
+        // Now a little magic to convert path into a complete URL. The Servlet
+        // 2.2 API does this automatically.
+
+        String absolutePath = getAbsoluteURL(path);
+
+        String encodedURL = _response.encodeRedirectURL(absolutePath);
+
+        _response.sendRedirect(encodedURL);
+    }
+
+    private void section(IMarkupWriter writer, String sectionName)
+    {
+        writer.begin("tr");
+        writer.attribute("class", "request-context-section");
+        writer.begin("th");
+        writer.attribute("colspan", 2);
+
+        writer.print(sectionName);
+        writer.end("tr");
+    }
+
+    private List getSorted(Enumeration e)
+    {
+        List result = new ArrayList();
+
+        // JDK 1.4 includes a helper method in Collections for
+        // this; but we want 1.2 compatibility for the
+        // forseable future.
+
+        while (e.hasMoreElements())
+            result.add(e.nextElement());
+
+        Collections.sort(result);
+
+        return result;
+    }
+
+    /**
+     * Writes the state of the context to the writer, typically for inclusion
+     * in a HTML page returned to the user. This is useful
+     * when debugging.  The Inspector uses this as well.
+     *
+     **/
+
+    public void write(IMarkupWriter writer)
+    {
+        // Create a box around all of this stuff ...
+
+        writer.begin("table");
+        writer.attribute("class", "request-context-border");
+        writer.begin("tr");
+        writer.begin("td");
+
+        // Get the session, if it exists, and display it.
+
+        HttpSession session = getSession();
+
+        if (session != null)
+        {
+            object(writer, "Session");
+            writer.begin("table");
+            writer.attribute("class", "request-context-object");
+
+            section(writer, "Properties");
+            header(writer, "Name", "Value");
+
+            pair(writer, "id", session.getId());
+            datePair(writer, "creationTime", session.getCreationTime());
+            datePair(writer, "lastAccessedTime", session.getLastAccessedTime());
+            pair(writer, "maxInactiveInterval", session.getMaxInactiveInterval());
+            pair(writer, "new", session.isNew());
+
+            List names = getSorted(session.getAttributeNames());
+            int count = names.size();
+
+            for (int i = 0; i < count; i++)
+            {
+                if (i == 0)
+                {
+                    section(writer, "Attributes");
+                    header(writer, "Name", "Value");
+                }
+
+                String name = (String) names.get(i);
+                pair(writer, name, session.getAttribute(name));
+            }
+
+            writer.end(); // Session
+
+        }
+
+        object(writer, "Request");
+        writer.begin("table");
+        writer.attribute("class", "request-context-object");
+
+        // Parameters ...
+
+        List parameters = getSorted(_request.getParameterNames());
+        int count = parameters.size();
+
+        for (int i = 0; i < count; i++)
+        {
+
+            if (i == 0)
+            {
+                section(writer, "Parameters");
+                header(writer, "Name", "Value(s)");
+            }
+
+            String name = (String) parameters.get(i);
+            String[] values = _request.getParameterValues(name);
+
+            writer.begin("tr");
+            writer.attribute("class", getRowClass());
+            writer.begin("th");
+            writer.print(name);
+            writer.end();
+            writer.begin("td");
+
+            if (values.length > 1)
+                writer.begin("ul");
+
+            for (int j = 0; j < values.length; j++)
+            {
+                if (values.length > 1)
+                    writer.beginEmpty("li");
+
+                writer.print(values[j]);
+
+            }
+
+            writer.end("tr");
+        }
+
+        section(writer, "Properties");
+        header(writer, "Name", "Value");
+
+        pair(writer, "authType", _request.getAuthType());
+        pair(writer, "characterEncoding", _request.getCharacterEncoding());
+        pair(writer, "contentLength", _request.getContentLength());
+        pair(writer, "contentType", _request.getContentType());
+        pair(writer, "method", _request.getMethod());
+        pair(writer, "pathInfo", _request.getPathInfo());
+        pair(writer, "pathTranslated", _request.getPathTranslated());
+        pair(writer, "protocol", _request.getProtocol());
+        pair(writer, "queryString", _request.getQueryString());
+        pair(writer, "remoteAddr", _request.getRemoteAddr());
+        pair(writer, "remoteHost", _request.getRemoteHost());
+        pair(writer, "remoteUser", _request.getRemoteUser());
+        pair(writer, "requestedSessionId", _request.getRequestedSessionId());
+        pair(writer, "requestedSessionIdFromCookie", _request.isRequestedSessionIdFromCookie());
+        pair(writer, "requestedSessionIdFromURL", _request.isRequestedSessionIdFromURL());
+        pair(writer, "requestedSessionIdValid", _request.isRequestedSessionIdValid());
+        pair(writer, "requestURI", _request.getRequestURI());
+        pair(writer, "scheme", _request.getScheme());
+        pair(writer, "serverName", _request.getServerName());
+        pair(writer, "serverPort", _request.getServerPort());
+        pair(writer, "contextPath", _request.getContextPath());
+        pair(writer, "servletPath", _request.getServletPath());
+
+        // Now deal with any headers
+
+        List headers = getSorted(_request.getHeaderNames());
+        count = headers.size();
+
+        for (int i = 0; i < count; i++)
+        {
+            if (i == 0)
+            {
+                section(writer, "Headers");
+                header(writer, "Name", "Value");
+            }
+
+            String name = (String) headers.get(i);
+            String value = _request.getHeader(name);
+
+            pair(writer, name, value);
+        }
+
+        // Attributes
+
+        List attributes = getSorted(_request.getAttributeNames());
+        count = attributes.size();
+
+        for (int i = 0; i < count; i++)
+        {
+            if (i == 0)
+            {
+                section(writer, "Attributes");
+                header(writer, "Name", "Value");
+            }
+
+            String name = (String) attributes.get(i);
+
+            pair(writer, name, _request.getAttribute(name));
+        }
+
+        // Cookies ...
+
+        Cookie[] cookies = _request.getCookies();
+
+        if (cookies != null)
+        {
+            for (int i = 0; i < cookies.length; i++)
+            {
+
+                if (i == 0)
+                {
+                    section(writer, "Cookies");
+                    header(writer, "Name", "Value");
+                }
+
+                Cookie cookie = cookies[i];
+
+                pair(writer, cookie.getName(), cookie.getValue());
+
+            } // Cookies loop
+        }
+
+        writer.end(); // Request
+
+        object(writer, "Servlet");
+        writer.begin("table");
+        writer.attribute("class", "request-context-object");
+
+        section(writer, "Properties");
+        header(writer, "Name", "Value");
+
+        pair(writer, "servlet", _servlet);
+        pair(writer, "name", _servlet.getServletName());
+        pair(writer, "servletInfo", _servlet.getServletInfo());
+
+        ServletConfig config = _servlet.getServletConfig();
+
+        List names = getSorted(config.getInitParameterNames());
+        count = names.size();
+
+        for (int i = 0; i < count; i++)
+        {
+
+            if (i == 0)
+            {
+                section(writer, "Init Parameters");
+                header(writer, "Name", "Value");
+            }
+
+            String name = (String) names.get(i);
+            ;
+            pair(writer, name, config.getInitParameter(name));
+
+        }
+
+        writer.end(); // Servlet
+
+        ServletContext context = config.getServletContext();
+
+        object(writer, "Servlet Context");
+        writer.begin("table");
+        writer.attribute("class", "request-context-object");
+
+        section(writer, "Properties");
+        header(writer, "Name", "Value");
+
+        pair(writer, "majorVersion", context.getMajorVersion());
+        pair(writer, "minorVersion", context.getMinorVersion());
+        pair(writer, "serverInfo", context.getServerInfo());
+
+        names = getSorted(context.getInitParameterNames());
+        count = names.size();
+        for (int i = 0; i < count; i++)
+        {
+            if (i == 0)
+            {
+                section(writer, "Initial Parameters");
+                header(writer, "Name", "Value");
+            }
+
+            String name = (String) names.get(i);
+            pair(writer, name, context.getInitParameter(name));
+        }
+
+        names = getSorted(context.getAttributeNames());
+        count = names.size();
+        for (int i = 0; i < count; i++)
+        {
+            if (i == 0)
+            {
+                section(writer, "Attributes");
+                header(writer, "Name", "Value");
+            }
+
+            String name = (String) names.get(i);
+            pair(writer, name, context.getAttribute(name));
+        }
+
+        writer.end(); // Servlet Context
+
+        writeSystemProperties(writer);
+
+        writer.end("table"); // The enclosing border
+    }
+
+    private void writeSystemProperties(IMarkupWriter writer)
+    {
+        Properties properties = null;
+
+        object(writer, "JVM System Properties");
+
+        try
+        {
+            properties = System.getProperties();
+        }
+        catch (SecurityException se)
+        {
+            writer.print("<p>");
+            writer.print(se.toString());
+            return;
+        }
+
+        String pathSeparator = System.getProperty("path.separator", ";");
+
+        writer.begin("table");
+        writer.attribute("class", "request-context-object");
+
+        List names = new ArrayList(properties.keySet());
+        Collections.sort(names);
+        int count = names.size();
+
+        for (int i = 0; i < count; i++)
+        {
+
+            if (i == 0)
+                header(writer, "Name", "Value");
+
+            String name = (String) names.get(i);
+
+            String property = properties.getProperty(name);
+
+            if (property != null && property.indexOf(pathSeparator) > 0 && name.endsWith(".path"))
+            {
+                writer.begin("tr");
+                writer.attribute("class", getRowClass());
+
+                writer.begin("th");
+                writer.print(name);
+                writer.end();
+
+                writer.begin("td");
+                writer.begin("ul");
+
+                StringTokenizer tokenizer = new StringTokenizer(property, pathSeparator);
+
+                while (tokenizer.hasMoreTokens())
+                {
+                    writer.beginEmpty("li");
+                    writer.print(tokenizer.nextToken());
+                }
+
+                writer.end("tr");
+            }
+            else
+            {
+                pair(writer, name, property);
+            }
+        }
+
+        writer.end(); // System Properties
+    }
+
+    /**
+     *  Invokes {@link #write(IMarkupWriter)}, which is used for debugging.
+     *  Does nothing if the cycle is rewinding.
+     *
+     **/
+
+    public void render(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        if (!cycle.isRewinding())
+            write(writer);
+    }
+
+    /**
+     *  Returns the multipart decoder and lazily decodes the request parameters.
+     *  This allows both for this operation to be performed only when really needed
+     *  and for opening the request for reading much later, so that the Engine can
+     *  have a chance to set the encoding that the request needs to use.
+     * 
+     *  @return the multipart decoder or null if not needed for this request
+     *  @since 3.0
+     **/
+    private IMultipartDecoder getDecoder()
+    {
+        if (_decoder != null && !_decoded) {
+            _decoder.decode(_request);
+            _decoded = true;
+        }
+
+        return _decoder;
+    }
+
+    /**
+     *  Sets the multipart decoder to be used for the request.
+     * 
+     *  @param decoder the multipart decoder
+     *  @since 3.0
+     **/
+    public void setDecoder(IMultipartDecoder decoder)
+    {
+        _decoder = decoder;
+        _decoded = false;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/request/ResponseOutputStream.java b/tapestry-framework/src/org/apache/tapestry/request/ResponseOutputStream.java
new file mode 100644
index 0000000..d220510
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/request/ResponseOutputStream.java
@@ -0,0 +1,298 @@
+//  Copyright 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.tapestry.request;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.SocketException;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  A special output stream works with a {@link HttpServletResponse}, buffering
+ *  data so as to defer opening the response's output stream.
+ *
+ *  <p>The buffering is pretty simple because the code
+ *  between {@link org.apache.tapestry.IMarkupWriter} and this shows lots of buffering
+ *  after the <code>PrintWriter</code> and inside the <code>OutputStreamWriter</code> that
+ *  can't be configured.
+ *
+ *  <p>This class performs some buffering, but it is not all that
+ *  useful because the 
+ *  {@link org.apache.tapestry.html.Body} component (which will
+ *  be used on virtually all Tapestry pages), buffers its wrapped contents
+ *  (that is, evertyhing inside the &lt;body&gt; tag in the generated HTML).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class ResponseOutputStream extends OutputStream
+{
+    private static final Log LOG = LogFactory.getLog(ResponseOutputStream.class);
+
+    /**
+     *  Default size for the buffer (2000 bytes).
+     *
+     **/
+
+    public static final int DEFAULT_SIZE = 2000;
+
+    private int _pos;
+    private int _maxSize;
+    private byte[] _buffer;
+
+    private String _contentType;
+    private HttpServletResponse _response;
+    private OutputStream _out;
+
+    private boolean _discard = false;
+
+    /**
+     *  Creates the stream with the default maximum buffer size.
+     *
+     **/
+
+    public ResponseOutputStream(HttpServletResponse response)
+    {
+        this(response, DEFAULT_SIZE);
+    }
+
+    /**
+     *  Standard constructor.
+     *
+     **/
+
+    public ResponseOutputStream(HttpServletResponse response, int maxSize)
+    {
+        _response = response;
+        _maxSize = maxSize;
+    }
+
+    /**
+     *  Does nothing.  This is because of chaining of <code>close()</code> from
+     *  {@link org.apache.tapestry.IMarkupWriter#close()} ... see {@link #flush()}.
+     * 
+     **/
+
+    public void close() throws IOException
+    {
+        // Does nothing.
+    }
+
+    /**
+     *  Flushes the underlying output stream, if is has been opened.  
+     *
+     *  <p>This method explicitly <em>does not</em> flush the internal buffer ...
+     *  that's because when an {@link org.apache.tapestry.IMarkupWriter} is closed (for instance, because
+     *  an exception is thrown), that <code>close()</code> spawns <code>flush()</code>es
+     *  and <code>close()</code>s throughout the output stream chain, eventually
+     *  reaching this method.
+     *
+     *  @see #forceFlush()
+     *
+     **/
+
+    public void flush() throws IOException
+    {
+        try
+        {
+            if (_out != null)
+                _out.flush();
+        }
+        catch (SocketException ex)
+        {
+            LOG.debug("Socket exception.");
+        }
+    }
+
+    /**
+     *  Writes the internal buffer to the output stream, opening it if necessary, then
+     *  flushes the output stream.  Future writes will go directly to the output stream.
+     *
+     **/
+
+    public void forceFlush() throws IOException
+    {
+        if (_out == null)
+        {
+
+            // In certain cases (such as when the Tapestry service sends a redirect),
+            // there is no output to send back (and no content type set).  In this
+            // case, forceFlush() does nothing.
+
+            if (_buffer == null)
+                return;
+
+            open();
+        }
+
+        try
+        {
+            _out.flush();
+        }
+        catch (SocketException ex)
+        {
+            LOG.debug("Socket exception.");
+        }
+    }
+
+    public String getContentType()
+    {
+        return _contentType;
+    }
+
+    public boolean getDiscard()
+    {
+        return _discard;
+    }
+
+    /**
+     *  Sets the response type to from the contentType property (which
+     *  defaults to "text/html") and gets an output stream
+     *  from the response, then writes the current buffer to it and
+     *  releases the buffer.
+     *
+     *  @throws IOException if the content type has never been set.
+     *
+     **/
+
+    private void open() throws IOException
+    {
+        if (_contentType == null)
+            throw new IOException(Tapestry.getMessage("ResponseOutputStream.content-type-not-set"));
+
+        _response.setContentType(_contentType);
+
+        _out = _response.getOutputStream();
+
+        innerWrite(_buffer, 0, _pos);
+
+        _pos = 0;
+        _buffer = null;
+    }
+
+    /**
+     *  Discards all output in the buffer.  This is used after an error to
+     *  restart the output (so that the error may be presented).
+     *
+     *  <p>Clears the discard flag.
+     *
+     **/
+
+    public void reset() throws IOException
+    {
+        _pos = 0;
+        _discard = false;
+    }
+
+    /**
+     *  Changes the maximum buffer size.  If the new buffer size is smaller
+     *  than the number of
+     *  bytes already in the buffer, the buffer is immediately flushed.
+     *
+     **/
+
+    public void setBufferSize(int value) throws IOException
+    {
+        if (value < _pos)
+        {
+            open();
+            return;
+        }
+
+        _maxSize = value;
+    }
+
+    public void setContentType(String value)
+    {
+        _contentType = value;
+    }
+
+    /**
+     *  Indicates whether the stream should ignore all data written to it.
+     *
+     **/
+
+    public void setDiscard(boolean value)
+    {
+        _discard = value;
+    }
+
+    private void innerWrite(byte[] b, int off, int len) throws IOException
+    {
+        if (b == null || len == 0 || _discard)
+            return;
+
+        try
+        {
+            _out.write(b, off, len);
+        }
+        catch (SocketException ex)
+        {
+            LOG.debug("Socket exception.");
+        }
+    }
+
+    public void write(byte b[], int off, int len) throws IOException
+    {
+        if (len == 0 || _discard)
+            return;
+
+        if (_out != null)
+        {
+            _out.write(b, off, len);
+            return;
+        }
+
+        // If too large for the maximum size buffer, then open the output stream
+        // write out and free the buffer, and write out the new stuff.
+
+        if (_pos + len >= _maxSize)
+        {
+            open();
+            innerWrite(b, off, len);
+            return;
+        }
+
+        // Allocate the buffer when it is initially needed.
+
+        if (_buffer == null)
+            _buffer = new byte[_maxSize];
+
+        // Copy the new bytes into the buffer and advance the position.
+
+        System.arraycopy(b, off, _buffer, _pos, len);
+        _pos += len;
+    }
+
+    public void write(int b) throws IOException
+    {
+        if (_discard)
+            return;
+
+        // This method is rarely called so this little inefficiency is better than
+        // maintaining that ugly buffer expansion code in two places.
+
+        byte[] tiny = new byte[] {(byte) b };
+
+        write(tiny, 0, 1);
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/resolver/AbstractSpecificationResolver.java b/tapestry-framework/src/org/apache/tapestry/resolver/AbstractSpecificationResolver.java
new file mode 100644
index 0000000..ac4cafe
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/resolver/AbstractSpecificationResolver.java
@@ -0,0 +1,200 @@
+//  Copyright 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.tapestry.resolver;
+
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.INamespace;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.engine.ISpecificationSource;
+import org.apache.tapestry.spec.IApplicationSpecification;
+import org.apache.tapestry.spec.IComponentSpecification;
+import org.apache.tapestry.util.pool.IPoolable;
+
+/**
+ *  Base class for resolving a {@link org.apache.tapestry.spec.IComponentSpecification}
+ *  for a particular page or component, within a specified 
+ *  {@link org.apache.tapestry.INamespace}.  In some cases, a search is necessary.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class AbstractSpecificationResolver implements IPoolable
+{
+    private ISpecificationSource _specificationSource;
+
+    private INamespace _namespace;
+
+    private IComponentSpecification _specification;
+
+    private IResourceLocation _applicationRootLocation;
+
+    private IResourceLocation _webInfLocation;
+
+    private IResourceLocation _webInfAppLocation;
+
+    private ISpecificationResolverDelegate _delegate;
+
+    public AbstractSpecificationResolver(IRequestCycle cycle)
+    {
+        IEngine engine = cycle.getEngine();
+
+        _specificationSource = engine.getSpecificationSource();
+
+        _applicationRootLocation = Tapestry.getApplicationRootLocation(cycle);
+
+        String servletName =
+            cycle.getRequestContext().getServlet().getServletConfig().getServletName();
+
+        _webInfLocation = _applicationRootLocation.getRelativeLocation("/WEB-INF/");
+
+        _webInfAppLocation = _webInfLocation.getRelativeLocation(servletName + "/");
+
+        IApplicationSpecification specification = engine.getSpecification();
+
+        if (specification.checkExtension(Tapestry.SPECIFICATION_RESOLVER_DELEGATE_EXTENSION_NAME))
+            _delegate =
+                (ISpecificationResolverDelegate) engine.getSpecification().getExtension(
+                    Tapestry.SPECIFICATION_RESOLVER_DELEGATE_EXTENSION_NAME,
+                    ISpecificationResolverDelegate.class);
+        else
+            _delegate = NullSpecificationResolverDelegate.getSharedInstance();
+    }
+
+    /**
+     *  Returns the {@link ISpecificationResolverDelegate} instance registered
+     *  in the application specification as extension
+     *  {@link Tapestry#SPECIFICATION_RESOLVER_DELEGATE_EXTENSION_NAME},
+     *  or null if no such extension exists.
+     * 
+     **/
+
+    public ISpecificationResolverDelegate getDelegate()
+    {
+        return _delegate;
+    }
+
+    /**
+     *  Returns the location of the servlet, within the
+     *  servlet context.
+     * 
+     **/
+
+    protected IResourceLocation getApplicationRootLocation()
+    {
+        return _applicationRootLocation;
+    }
+
+    /**
+     *  Invoked in subclasses to identify the resolved namespace.
+     * 
+     **/
+
+    protected void setNamespace(INamespace namespace)
+    {
+        _namespace = namespace;
+    }
+
+    /**
+     *  Returns the resolve namespace.
+     * 
+     **/
+
+    public INamespace getNamespace()
+    {
+        return _namespace;
+    }
+
+    /**
+     *  Returns the specification source for the running application.
+     * 
+     **/
+
+    protected ISpecificationSource getSpecificationSource()
+    {
+        return _specificationSource;
+    }
+
+    /**
+     *  Returns the location of /WEB-INF/, in the servlet context.
+     * 
+     **/
+
+    protected IResourceLocation getWebInfLocation()
+    {
+        return _webInfLocation;
+    }
+
+    /**
+     *  Returns the location of the application-specific subdirectory, under
+     *  /WEB-INF/, in the servlet context.
+     * 
+     **/
+
+    protected IResourceLocation getWebInfAppLocation()
+    {
+        return _webInfAppLocation;
+    }
+
+    /**
+     *  Returns the resolved specification.
+     * 
+     **/
+
+    public IComponentSpecification getSpecification()
+    {
+        return _specification;
+    }
+
+    /**
+     *  Invoked in subclass to set the final specification the initial
+     *  inputs are resolved to.
+     * 
+     **/
+
+    protected void setSpecification(IComponentSpecification specification)
+    {
+        _specification = specification;
+    }
+
+    /**
+     *  Clears the namespace, specification and simpleName properties.
+     * 
+     **/
+
+    protected void reset()
+    {
+        _namespace = null;
+        _specification = null;
+    }
+
+    /** Does nothing. */
+    public void discardFromPool()
+    {
+
+    }
+
+    /** Invokes {@link #reset()} */
+
+    public void resetForPool()
+    {
+        reset();
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/resolver/ComponentSpecificationResolver.java b/tapestry-framework/src/org/apache/tapestry/resolver/ComponentSpecificationResolver.java
new file mode 100644
index 0000000..e28b3dc
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/resolver/ComponentSpecificationResolver.java
@@ -0,0 +1,259 @@
+//  Copyright 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.tapestry.resolver;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.INamespace;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.spec.IComponentSpecification;
+
+/**
+ *  Utility class that understands the rules of component types (which
+ *  may optionally have a library prefix) and can resolve 
+ *  the type to a {@link org.apache.tapestry.INamespace} and a 
+ *  {@link org.apache.tapestry.spec.IComponentSpecification}.
+ * 
+ *  <p>Like {@link org.apache.tapestry.resolver.PageSpecificationResolver},
+ *  if the component is not defined explicitly in the namespace, a search
+ *  may occur:
+ * 
+ *  Performs the tricky work of resolving a page name to a page specification.
+ *  The search for pages in the application namespace is the most complicated,
+ *  since Tapestry searches for pages that aren't explicitly defined in the
+ *  application specification.  The search, based on the <i>simple-name</i>
+ *  of the page, goes as follows:
+ * 
+ *  <ul>
+ *  <li>As declared in the application specification
+ *  <li><i>type</i>.jwc in the same folder as the application specification
+ *  <li><i>type</i> jwc in the WEB-INF/<i>servlet-name</i> directory of the context root
+ *  <li><i>type</i>.jwc in WEB-INF
+ *  <li><i>type</i>.jwc in the application root (within the context root)
+ *  <li>By searching the framework namespace
+ *  </ul> 
+ * 
+ *  The search for components in library namespaces is more abbreviated:
+ *  <li>As declared in the library specification
+ *  <li><i>type</i>.jwc in the same folder as the library specification
+ *  <li>By searching the framework namespace
+ *  </ul>
+ *
+ * 
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class ComponentSpecificationResolver extends AbstractSpecificationResolver
+{
+    private static final Log LOG = LogFactory.getLog(ComponentSpecificationResolver.class);
+
+    private String _type;
+
+    public ComponentSpecificationResolver(IRequestCycle cycle)
+    {
+        super(cycle);
+    }
+
+    protected void reset()
+    {
+        _type = null;
+
+        super.reset();
+    }
+
+    /**
+     *  Passed the namespace of a container (to resolve the type in)
+     *  and the type to resolve, performs the processing.  A "bare type"
+     *  (without a library prefix) may be in the containerNamespace,
+     *  or the framework namespace
+     *  (a search occurs in that order).
+     * 
+     *  @param cycle current request cycle
+     *  @param containerNamespace namespace that may contain
+     *  a library referenced in the type
+     *  @param type the component specification
+     *  to  find, either a simple name, or prefixed with a library id
+     *  (defined for the container namespace)
+     * 
+     *  @see #getNamespace()
+     *  @see #getSpecification()
+     * 
+     **/
+
+    public void resolve(
+        IRequestCycle cycle,
+        INamespace containerNamespace,
+        String type,
+        ILocation location)
+    {
+        int colonx = type.indexOf(':');
+
+        if (colonx > 0)
+        {
+            String libraryId = type.substring(0, colonx);
+            String simpleType = type.substring(colonx + 1);
+
+            resolve(cycle, containerNamespace, libraryId, simpleType, location);
+        }
+        else
+            resolve(cycle, containerNamespace, null, type, location);
+    }
+
+    /**
+     *  Like {@link #resolve(org.apache.tapestry.IRequestCycle, org.apache.tapestry.INamespace, java.lang.String, org.apache.tapestry.ILocation)},
+     *  but used when the type has already been parsed into a library id and a simple type.
+     * 
+     *  @param cycle current request cycle
+     *  @param containerNamespace namespace that may contain
+     *  a library referenced in the type
+     *  @param libraryId the library id within the container namespace, or null
+     *  @param type the component specification
+     *  to  find as a simple name (without a library prefix)
+     *  @param location of reference to be resolved
+     *  @throws ApplicationRuntimeException if the type cannot be resolved
+     * 
+     **/
+
+    public void resolve(
+        IRequestCycle cycle,
+        INamespace containerNamespace,
+        String libraryId,
+        String type,
+        ILocation location)
+    {
+        reset();
+        _type = type;
+
+        INamespace namespace = null;
+
+        if (libraryId != null)
+            namespace = containerNamespace.getChildNamespace(libraryId);
+        else
+            namespace = containerNamespace;
+
+        setNamespace(namespace);
+
+        if (namespace.containsComponentType(type))
+            setSpecification(namespace.getComponentSpecification(type));
+        else
+            searchForComponent(cycle);
+
+        // If not found after search, check to see if it's in
+        // the framework instead.
+
+        if (getSpecification() == null)
+        {
+
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "Namespace.no-such-component-type",
+                    type,
+                    namespace.getNamespaceId()),
+                location,
+                null);
+
+        }
+    }
+
+    private void searchForComponent(IRequestCycle cycle)
+    {
+        INamespace namespace = getNamespace();
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Resolving unknown component '" + _type + "' in " + namespace);
+
+        String expectedName = _type + ".jwc";
+        IResourceLocation namespaceLocation = namespace.getSpecificationLocation();
+
+        // Look for appropriate file in same folder as the library (or application)
+        // specificaiton.
+
+        if (found(namespaceLocation.getRelativeLocation(expectedName)))
+            return;
+
+        if (namespace.isApplicationNamespace())
+        {
+
+            // The application namespace gets some extra searching.
+
+            if (found(getWebInfAppLocation().getRelativeLocation(expectedName)))
+                return;
+
+            if (found(getWebInfLocation().getRelativeLocation(expectedName)))
+                return;
+
+            if (found(getApplicationRootLocation().getRelativeLocation(expectedName)))
+                return;
+        }
+
+        // Not in the library or app spec; does it match a component
+        // provided by the Framework?
+
+        INamespace framework = getSpecificationSource().getFrameworkNamespace();
+
+        if (framework.containsComponentType(_type))
+        {
+            setSpecification(framework.getComponentSpecification(_type));
+            return;
+        }
+
+        IComponentSpecification specification =
+            getDelegate().findComponentSpecification(cycle, namespace, _type);
+
+        setSpecification(specification);
+
+        // If not found by here, an exception will be thrown.
+    }
+
+    private boolean found(IResourceLocation location)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Checking: " + location);
+
+        if (location.getResourceURL() == null)
+            return false;
+
+        setSpecification(getSpecificationSource().getComponentSpecification(location));
+
+        install();
+
+        return true;
+    }
+
+    private void install()
+    {
+        INamespace namespace = getNamespace();
+        IComponentSpecification specification = getSpecification();
+
+        if (LOG.isDebugEnabled())
+            LOG.debug(
+                "Installing component type "
+                    + _type
+                    + " into "
+                    + namespace
+                    + " as "
+                    + specification);
+
+        namespace.installComponentSpecification(_type, specification);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/resolver/ISpecificationResolverDelegate.java b/tapestry-framework/src/org/apache/tapestry/resolver/ISpecificationResolverDelegate.java
new file mode 100644
index 0000000..1cf1525
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/resolver/ISpecificationResolverDelegate.java
@@ -0,0 +1,72 @@
+//  Copyright 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.tapestry.resolver;
+
+import org.apache.tapestry.INamespace;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.spec.IComponentSpecification;
+
+/**
+ *  Delegate interface used when a page or component specification
+ *  can not be found by the normal means.  This allows hooks
+ *  to support specifications from unusual locations, or generated
+ *  on the fly.
+ * 
+ *  <p>The delegate must be coded in a threadsafe manner.
+ * 
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public interface ISpecificationResolverDelegate
+{
+    /**
+     *  Invoked by {@link PageSpecificationResolver} to find the indicated
+     *  page specification.  Returns
+     *  the specification, or null.  The specification, if returned, is not cached by Tapestry
+     *  (it is up to the delegate to cache the specification if desired).
+     * 
+     *  @param cycle used to gain access to framework and Servlet API objects
+     *  @param namespace the namespace containing the page
+     *  @param simplePageName the name of the page (without any namespace prefix)
+     * 
+     **/
+
+    public IComponentSpecification findPageSpecification(
+        IRequestCycle cycle,
+        INamespace namespace,
+        String simplePageName);
+
+    /**
+     *  Invoked by {@link PageSpecificationResolver} to find the indicated
+     *  component specification.  Returns
+     *  the specification, or null.  The specification, if returned, is not cached by Tapestry
+     *  (it is up to the delegate to cache the specification if desired).
+     * 
+     *  <p>The delegate must be coded in a threadsafe manner.
+     * 
+     *  @param cycle used to gain access to framework and Servlet API objects
+     *  @param namespace the namespace containing the component
+     *  @param type the component type (without any namespace prefix)
+     * 
+     **/
+
+    public IComponentSpecification findComponentSpecification(
+        IRequestCycle cycle,
+        INamespace namespace,
+        String type);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/resolver/NullSpecificationResolverDelegate.java b/tapestry-framework/src/org/apache/tapestry/resolver/NullSpecificationResolverDelegate.java
new file mode 100644
index 0000000..d4731d6
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/resolver/NullSpecificationResolverDelegate.java
@@ -0,0 +1,68 @@
+//  Copyright 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.tapestry.resolver;
+
+import org.apache.tapestry.INamespace;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.spec.IComponentSpecification;
+
+/**
+ *  Stand-in class used when the application fails to specify an actual delegate.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class NullSpecificationResolverDelegate implements ISpecificationResolverDelegate
+{
+    private static NullSpecificationResolverDelegate _shared;
+
+    public static NullSpecificationResolverDelegate getSharedInstance()
+    {
+        if (_shared == null)
+            _shared = new NullSpecificationResolverDelegate();
+
+        return _shared;
+    }
+
+    /**
+     *  Returns null.
+     * 
+     **/
+
+    public IComponentSpecification findPageSpecification(
+        IRequestCycle cycle,
+        INamespace namespace,
+        String simplePageName)
+    {
+        return null;
+    }
+
+    /**
+     *  Returns null.
+     * 
+     **/
+
+    public IComponentSpecification findComponentSpecification(
+        IRequestCycle cycle,
+        INamespace namespace,
+        String type)
+    {
+        return null;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/resolver/PageSpecificationResolver.java b/tapestry-framework/src/org/apache/tapestry/resolver/PageSpecificationResolver.java
new file mode 100644
index 0000000..5c3cfa1
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/resolver/PageSpecificationResolver.java
@@ -0,0 +1,273 @@
+//  Copyright 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.tapestry.resolver;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.INamespace;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.spec.ComponentSpecification;
+import org.apache.tapestry.spec.IComponentSpecification;
+
+/**
+ *  Performs the tricky work of resolving a page name to a page specification.
+ *  The search for pages in the application namespace is the most complicated,
+ *  since Tapestry searches for pages that aren't explicitly defined in the
+ *  application specification.  The search, based on the <i>simple-name</i>
+ *  of the page, goes as follows:
+ * 
+ *  <ul>
+ *  <li>As declared in the application specification
+ *  <li><i>simple-name</i>.page in the same folder as the application specification
+ *  <li><i>simple-name</i> page in the WEB-INF/<i>servlet-name</i> directory of the context root
+ *  <li><i>simple-name</i>.page in WEB-INF
+ *  <li><i>simple-name</i>.page in the application root (within the context root)
+ *  <li><i>simple-name</i>.html as a template in the application root, 
+ *      for which an implicit specification is generated
+ *  <li>By searching the framework namespace
+ *  <li>By invoking {@link org.apache.tapestry.resolver.ISpecificationResolverDelegate#findPageSpecification(IRequestCycle, INamespace, String)}
+ *  </ul>
+ * 
+ *  <p>Pages in a component library are searched for in a more abbreviated fashion:
+ *  <ul>
+ *  <li>As declared in the library specification
+ *  <li><i>simple-name</i>.page in the same folder as the library specification
+ *  <li>By searching the framework namespace
+ *  <li>By invoking {@link org.apache.tapestry.resolver.ISpecificationResolverDelegate#findPageSpecification(IRequestCycle, INamespace, String)}
+ *  </ul>
+ *
+ *  @see org.apache.tapestry.engine.IPageSource
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class PageSpecificationResolver extends AbstractSpecificationResolver
+{
+    private static final Log LOG = LogFactory.getLog(PageSpecificationResolver.class);
+
+    private String _simpleName;
+
+    public PageSpecificationResolver(IRequestCycle cycle)
+    {
+        super(cycle);
+    }
+
+    /**
+     *  Resolve the name (which may have a library id prefix) to a namespace
+     *  (see {@link #getNamespace()}) and a specification (see {@link #getSpecification()}).
+     * 
+     *  @throws ApplicationRuntimeException if the name cannot be resolved
+     * 
+     **/
+
+    public void resolve(IRequestCycle cycle, String prefixedName)
+    {
+        reset();
+
+        INamespace namespace = null;
+
+        int colonx = prefixedName.indexOf(':');
+
+        if (colonx > 0)
+        {
+            _simpleName = prefixedName.substring(colonx + 1);
+            String namespaceId = prefixedName.substring(0, colonx);
+
+            if (namespaceId.equals(INamespace.FRAMEWORK_NAMESPACE))
+                namespace = getSpecificationSource().getFrameworkNamespace();
+            else
+                namespace =
+                    getSpecificationSource().getApplicationNamespace().getChildNamespace(
+                        namespaceId);
+        }
+        else
+        {
+            _simpleName = prefixedName;
+
+            namespace = getSpecificationSource().getApplicationNamespace();
+        }
+
+        setNamespace(namespace);
+
+        if (namespace.containsPage(_simpleName))
+        {
+            setSpecification(namespace.getPageSpecification(_simpleName));
+            return;
+        }
+
+        // Not defined in the specification, so it's time to hunt it down.
+
+        searchForPage(cycle);
+
+        if (getSpecification() == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "Namespace.no-such-page",
+                    _simpleName,
+                    namespace.getNamespaceId()));
+
+    }
+
+    public String getSimplePageName()
+    {
+        return _simpleName;
+    }
+
+    private void searchForPage(IRequestCycle cycle)
+    {
+        INamespace namespace = getNamespace();
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Resolving unknown page '" + _simpleName + "' in " + namespace);
+
+        String expectedName = _simpleName + ".page";
+
+        IResourceLocation namespaceLocation = namespace.getSpecificationLocation();
+
+        // See if there's a specification file in the same folder
+        // as the library or application specification that's
+        // supposed to contain the page.
+
+        if (found(namespaceLocation.getRelativeLocation(expectedName)))
+            return;
+
+        if (namespace.isApplicationNamespace())
+        {
+
+            // The application namespace gets some extra searching.
+
+            if (found(getWebInfAppLocation().getRelativeLocation(expectedName)))
+                return;
+
+            if (found(getWebInfLocation().getRelativeLocation(expectedName)))
+                return;
+
+            if (found(getApplicationRootLocation().getRelativeLocation(expectedName)))
+                return;
+
+            // The wierd one ... where we see if there's a template in the application root location.
+
+            String templateName = _simpleName + "." + getTemplateExtension();
+
+            IResourceLocation templateLocation =
+                getApplicationRootLocation().getRelativeLocation(templateName);
+
+            if (templateLocation.getResourceURL() != null)
+            {
+                setupImplicitPage(templateLocation);
+                return;
+            }
+
+            // Not found in application namespace, so maybe its a framework page.
+
+            INamespace framework = getSpecificationSource().getFrameworkNamespace();
+
+            if (framework.containsPage(_simpleName))
+            {
+                if (LOG.isDebugEnabled())
+                    LOG.debug("Found " + _simpleName + " in framework namespace.");
+
+                setNamespace(framework);
+
+                // Note:  This implies that normal lookup rules don't work
+                // for the framework!  Framework pages must be
+                // defined in the framework library specification.
+
+                setSpecification(framework.getPageSpecification(_simpleName));
+                return;
+            }
+        }
+
+        // Not found by any normal rule, so its time to
+        // consult the delegate.
+
+        IComponentSpecification specification =
+            getDelegate().findPageSpecification(cycle, namespace, _simpleName);
+            
+        setSpecification(specification);
+    }
+
+    private void setupImplicitPage(IResourceLocation location)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Found HTML template at " + location);
+		// TODO The SpecFactory in Specification parser should be used in some way to create an IComponentSpecifciation!
+        IComponentSpecification specification = new ComponentSpecification();
+        specification.setPageSpecification(true);
+        specification.setSpecificationLocation(location);
+
+        setSpecification(specification);
+
+        install();
+    }
+
+    private boolean found(IResourceLocation location)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Checking: " + location);
+
+        if (location.getResourceURL() == null)
+            return false;
+
+        setSpecification(getSpecificationSource().getPageSpecification(location));
+
+        install();
+
+        return true;
+    }
+
+    private void install()
+    {
+        INamespace namespace = getNamespace();
+        IComponentSpecification specification = getSpecification();
+
+        if (LOG.isDebugEnabled())
+            LOG.debug(
+                "Installing page " + _simpleName + " into " + namespace + " as " + specification);
+
+        namespace.installPageSpecification(_simpleName, specification);
+    }
+
+    /**
+     *  If the namespace defines the template extension (as property
+     *  {@link Tapestry#TEMPLATE_EXTENSION_PROPERTY}, then that is used, otherwise
+     *  the default is used.
+     * 
+     **/
+
+    private String getTemplateExtension()
+    {
+        String extension =
+            getNamespace().getSpecification().getProperty(Tapestry.TEMPLATE_EXTENSION_PROPERTY);
+
+        if (extension == null)
+            extension = Tapestry.DEFAULT_TEMPLATE_EXTENSION;
+
+        return extension;
+    }
+    
+    protected void reset()
+    {
+    	_simpleName = null;
+    	
+        super.reset();
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/resource/AbstractResourceLocation.java b/tapestry-framework/src/org/apache/tapestry/resource/AbstractResourceLocation.java
new file mode 100644
index 0000000..0aff8d5
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/resource/AbstractResourceLocation.java
@@ -0,0 +1,108 @@
+//  Copyright 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.tapestry.resource;
+
+import java.util.Locale;
+
+import org.apache.tapestry.IResourceLocation;
+
+public abstract class AbstractResourceLocation implements IResourceLocation
+{
+    private String _path;
+    private String _name;
+    private String _folderPath;
+    private Locale _locale;
+
+    protected AbstractResourceLocation(String path)
+    {
+        this(path, null);
+    }
+
+    protected AbstractResourceLocation(String path, Locale locale)
+    {
+        _path = path;
+        _locale = locale;
+    }
+
+    public String getName()
+    {
+        if (_name == null)
+            split();
+
+        return _name;
+    }
+
+    public IResourceLocation getRelativeLocation(String name)
+    {
+        if (name.startsWith("/"))
+        {
+            if (name.equals(_path))
+                return this;
+
+            return buildNewResourceLocation(name);
+        }
+
+        if (_folderPath == null)
+            split();
+
+        if (name.equals(_name))
+            return this;
+
+        return buildNewResourceLocation(_folderPath + name);
+    }
+
+    public String getPath()
+    {
+        return _path;
+    }
+
+    public Locale getLocale()
+    {
+        return _locale;
+    }
+
+
+    protected abstract IResourceLocation buildNewResourceLocation(String path);
+
+    private void split()
+    {
+        int lastSlashx = _path.lastIndexOf('/');
+
+        _folderPath = _path.substring(0, lastSlashx + 1);
+        _name = _path.substring(lastSlashx + 1);
+    }
+
+
+    /**
+     *  Returns true if the other object is an instance of the
+     *  same class, and the paths are equal.
+     * 
+     **/
+
+    public boolean equals(Object obj)
+    {
+        if (obj == null)
+            return false;
+
+        if (obj.getClass().equals(getClass()))
+        {
+            AbstractResourceLocation otherLocation = (AbstractResourceLocation) obj;
+
+            return _path.equals(otherLocation._path);
+        }
+
+        return false;
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/resource/ClasspathResourceLocation.java b/tapestry-framework/src/org/apache/tapestry/resource/ClasspathResourceLocation.java
new file mode 100644
index 0000000..5586dc1
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/resource/ClasspathResourceLocation.java
@@ -0,0 +1,110 @@
+//  Copyright 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.tapestry.resource;
+
+import java.net.URL;
+import java.util.Locale;
+
+import org.apache.commons.lang.builder.HashCodeBuilder;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.util.LocalizedResource;
+import org.apache.tapestry.util.LocalizedResourceFinder;
+
+/**
+ *  Implementation of {@link org.apache.tapestry.IResourceLocation}
+ *  for resources found within the classpath.
+ * 
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class ClasspathResourceLocation extends AbstractResourceLocation
+{
+    private IResourceResolver _resolver;
+
+    public ClasspathResourceLocation(IResourceResolver resolver, String path)
+    {
+        this(resolver, path, null);
+    }
+
+    public ClasspathResourceLocation(IResourceResolver resolver, String path, Locale locale)
+    {
+        super(path, locale);
+
+        _resolver = resolver;
+    }
+
+    /**
+     *  Locates the localization of the
+     *  resource using {@link org.apache.tapestry.util.LocalizedResourceFinder}
+     * 
+     **/
+
+    public IResourceLocation getLocalization(Locale locale)
+    {
+        String path = getPath();
+        LocalizedResourceFinder finder = new LocalizedResourceFinder(_resolver);
+
+        LocalizedResource localizedResource = finder.resolve(path, locale);
+
+        if (localizedResource == null)
+            return null;
+
+        String localizedPath = localizedResource.getResourcePath();
+        Locale pathLocale = localizedResource.getResourceLocale();
+
+        if (localizedPath == null)
+            return null;
+
+        if (path.equals(localizedPath))
+            return this;
+
+        return new ClasspathResourceLocation(_resolver, localizedPath, pathLocale);
+    }
+
+    /**
+     *  Invokes {@link IResourceResolver#getResource(String)}
+     * 
+     **/
+
+    public URL getResourceURL()
+    {
+        return _resolver.getResource(getPath());
+    }
+
+    public String toString()
+    {
+        return "classpath:" + getPath();
+    }
+
+    public int hashCode()
+    {
+        HashCodeBuilder builder = new HashCodeBuilder(4783, 23);
+
+        builder.append(getPath());
+
+        return builder.toHashCode();
+    }
+
+    protected IResourceLocation buildNewResourceLocation(String path)
+    {
+        return new ClasspathResourceLocation(_resolver, path);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/resource/ContextResourceLocation.java b/tapestry-framework/src/org/apache/tapestry/resource/ContextResourceLocation.java
new file mode 100644
index 0000000..4dfbb31
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/resource/ContextResourceLocation.java
@@ -0,0 +1,125 @@
+//  Copyright 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.tapestry.resource;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Locale;
+
+import javax.servlet.ServletContext;
+
+import org.apache.commons.lang.builder.HashCodeBuilder;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.util.LocalizedContextResourceFinder;
+import org.apache.tapestry.util.LocalizedResource;
+
+/**
+ *  Implementation of {@link org.apache.tapestry.IResourceLocation}
+ *  for resources found within the web application context.
+ * 
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class ContextResourceLocation extends AbstractResourceLocation
+{
+    private static final Log LOG = LogFactory.getLog(ContextResourceLocation.class);
+
+    private ServletContext _context;
+
+    public ContextResourceLocation(ServletContext context, String path)
+    {
+        this(context, path, null);
+    }
+
+    public ContextResourceLocation(ServletContext context, String path, Locale locale)
+    {
+        super(path, locale);
+
+        _context = context;
+    }
+
+    /**
+     *  Locates the resource using {@link LocalizedContextResourceFinder}
+     *  and {@link ServletContext#getResource(java.lang.String)}.
+     * 
+     **/
+
+    public IResourceLocation getLocalization(Locale locale)
+    {
+        LocalizedContextResourceFinder finder = new LocalizedContextResourceFinder(_context);
+
+        String path = getPath();
+        LocalizedResource localizedResource = finder.resolve(path, locale);
+
+        if (localizedResource == null)
+            return null;
+
+        String localizedPath = localizedResource.getResourcePath();
+        Locale pathLocale = localizedResource.getResourceLocale();
+
+        if (localizedPath == null)
+            return null;
+
+        if (path.equals(localizedPath))
+            return this;
+
+        return new ContextResourceLocation(_context, localizedPath, pathLocale);
+    }
+
+    public URL getResourceURL()
+    {
+        try
+        {
+            return _context.getResource(getPath());
+        }
+        catch (MalformedURLException ex)
+        {
+            LOG.warn(
+                Tapestry.format(
+                    "ContextResourceLocation.unable-to-reference-context-path",
+                    getPath()),
+                ex);
+
+            return null;
+        }
+    }
+
+    public String toString()
+    {
+        return "context:" + getPath();
+    }
+
+    public int hashCode()
+    {
+        HashCodeBuilder builder = new HashCodeBuilder(3265, 143);
+
+        builder.append(getPath());
+
+        return builder.toHashCode();
+    }
+
+    protected IResourceLocation buildNewResourceLocation(String path)
+    {
+        return new ContextResourceLocation(_context, path);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/script/AbstractToken.java b/tapestry-framework/src/org/apache/tapestry/script/AbstractToken.java
new file mode 100644
index 0000000..77e5570
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/AbstractToken.java
@@ -0,0 +1,99 @@
+//  Copyright 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.tapestry.script;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.util.prop.OgnlUtils;
+
+/**
+ *  Base class for creating tokens which may contain other tokens.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 0.2.9
+ * 
+ **/
+
+abstract class AbstractToken implements IScriptToken
+{
+    private List _tokens;
+    private ILocation _location;
+    private IResourceResolver _resolver;
+
+    protected AbstractToken(ILocation location)
+    {
+        _location = location;
+    }
+
+    public ILocation getLocation()
+    {
+        return _location;
+    }
+
+    public void addToken(IScriptToken token)
+    {
+        if (_tokens == null)
+            _tokens = new ArrayList();
+
+        _tokens.add(token);
+    }
+
+    /**
+     *  Invokes {@link IScriptToken#write(StringBuffer,ScriptSession)}
+     *  on each child token (if there are any).
+     *
+     **/
+
+    protected void writeChildren(StringBuffer buffer, ScriptSession session)
+    {
+        if (_tokens == null)
+            return;
+
+        Iterator i = _tokens.iterator();
+
+        while (i.hasNext())
+        {
+            IScriptToken token = (IScriptToken) i.next();
+
+            token.write(buffer, session);
+        }
+    }
+
+    /**
+     * Evaluates the expression against the session's symbols, using
+     * {@link OgnlUtils#get(String, ClassResolver, Object)} and
+     * returns the result.
+     */
+    protected Object evaluate(String expression, ScriptSession session)
+    {
+        if (_resolver == null)
+            _resolver = session.getRequestCycle().getEngine().getResourceResolver();
+
+        try
+        {
+            return OgnlUtils.get(expression, _resolver, session.getSymbols());
+        }
+        catch (Exception ex)
+        {
+            throw new ApplicationRuntimeException(ex.getMessage(), _location, ex);
+        }
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/script/AbstractTokenRule.java b/tapestry-framework/src/org/apache/tapestry/script/AbstractTokenRule.java
new file mode 100644
index 0000000..1ed6139
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/AbstractTokenRule.java
@@ -0,0 +1,192 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.util.xml.BaseRule;
+import org.apache.tapestry.util.xml.RuleDirectedParser;
+
+/**
+ * Base class for the rules that build {@link org.apache.tapestry.script.IScriptToken}s.
+ * Used with classes that can contain a mix of text and elements (those that
+ * accept "full content").
+ * 
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * @since 3.0
+ **/
+
+abstract class AbstractTokenRule extends BaseRule
+{
+
+    /**
+     * Adds a token to its parent, the top object on the stack.
+     */
+    protected void addToParent(RuleDirectedParser parser, IScriptToken token)
+    {
+        IScriptToken parent = (IScriptToken) parser.peek();
+
+        parent.addToken(token);
+    }
+
+    /**
+     * Peeks at the top object on the stack (which must be a {@link IScriptToken}),
+     * and converts the text into a series of {@link org.apache.tapestry.script.StaticToken} and
+     * {@link org.apache.tapestry.script.InsertToken}s.
+     */
+
+    public void content(RuleDirectedParser parser, String content)
+    {
+        IScriptToken token = (IScriptToken) parser.peek();
+
+        addTextTokens(token, content, parser.getLocation());
+    }
+
+    private static final int STATE_START = 0;
+    private static final int STATE_DOLLAR = 1;
+    private static final int STATE_COLLECT_EXPRESSION = 2;
+
+    /**
+     * Parses the provided text and converts it into a series of 
+     */
+    protected void addTextTokens(IScriptToken token, String text, ILocation location)
+    {
+        char[] buffer = text.toCharArray();
+        int state = STATE_START;
+        int blockStart = 0;
+        int blockLength = 0;
+        int expressionStart = -1;
+        int expressionLength = 0;
+        int i = 0;
+        int braceDepth = 0;
+
+        while (i < buffer.length)
+        {
+            char ch = buffer[i];
+
+            switch (state)
+            {
+                case STATE_START :
+
+                    if (ch == '$')
+                    {
+                        state = STATE_DOLLAR;
+                        i++;
+                        continue;
+                    }
+
+                    blockLength++;
+                    i++;
+                    continue;
+
+                case STATE_DOLLAR :
+
+                    if (ch == '{')
+                    {
+                        state = STATE_COLLECT_EXPRESSION;
+                        i++;
+
+                        expressionStart = i;
+                        expressionLength = 0;
+                        braceDepth = 1;
+
+                        continue;
+                    }
+
+                    // The '$' was just what it was, not the start of a ${} expression
+                    // block, so include it as part of the static text block.
+
+                    blockLength++;
+
+                    state = STATE_START;
+                    continue;
+
+                case STATE_COLLECT_EXPRESSION :
+
+                    if (ch != '}')
+                    {
+                        if (ch == '{')
+                            braceDepth++;
+
+                        i++;
+                        expressionLength++;
+                        continue;
+                    }
+
+                    braceDepth--;
+
+                    if (braceDepth > 0)
+                    {
+                        i++;
+                        expressionLength++;
+                        continue;
+                    }
+
+                    // Hit the closing brace of an expression.
+
+                    // Degenerate case:  the string "${}".
+
+                    if (expressionLength == 0)
+                        blockLength += 3;
+
+                    if (blockLength > 0)
+                        token.addToken(constructStatic(text, blockStart, blockLength, location));
+
+                    if (expressionLength > 0)
+                    {
+                        String expression =
+                            text.substring(expressionStart, expressionStart + expressionLength);
+
+                        token.addToken(new InsertToken(expression, location));
+                    }
+
+                    i++;
+                    blockStart = i;
+                    blockLength = 0;
+
+                    // And drop into state start
+
+                    state = STATE_START;
+
+                    continue;
+            }
+
+        }
+
+        // OK, to handle the end.  Couple of degenerate cases where
+        // a ${...} was incomplete, so we adust the block length.
+
+        if (state == STATE_DOLLAR)
+            blockLength++;
+
+        if (state == STATE_COLLECT_EXPRESSION)
+            blockLength += expressionLength + 2;
+
+        if (blockLength > 0)
+            token.addToken(constructStatic(text, blockStart, blockLength, location));
+    }
+
+    private IScriptToken constructStatic(
+        String text,
+        int blockStart,
+        int blockLength,
+        ILocation location)
+    {
+        String literal = text.substring(blockStart, blockStart + blockLength);
+
+        return new StaticToken(literal, location);
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/script/BodyRule.java b/tapestry-framework/src/org/apache/tapestry/script/BodyRule.java
new file mode 100644
index 0000000..0e0ddfe
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/BodyRule.java
@@ -0,0 +1,43 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.util.xml.RuleDirectedParser;
+import org.xml.sax.Attributes;
+
+/**
+ * Constructs a {@link org.apache.tapestry.script.BodyToken} from
+ * a &lt;body&gt; element, which contains full content.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * @since 3.0
+ */
+class BodyRule extends AbstractTokenRule
+{
+    public void startElement(RuleDirectedParser parser, Attributes attributes)
+    {
+        BodyToken token = new BodyToken(parser.getLocation());
+        addToParent(parser, token);
+
+        parser.push(token);
+    }
+
+    public void endElement(RuleDirectedParser parser)
+    {
+        parser.pop();
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/script/BodyToken.java b/tapestry-framework/src/org/apache/tapestry/script/BodyToken.java
new file mode 100644
index 0000000..2bb18d3
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/BodyToken.java
@@ -0,0 +1,55 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.ILocation;
+
+
+/**
+ *  Generates a String from its child tokens, then applies it
+ *  to {@link ScriptSession#setBody(String)}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 0.2.9
+ * 
+ **/
+
+class BodyToken extends AbstractToken
+{
+    private int _bufferLengthHighwater = 100;
+
+	public BodyToken(ILocation location)
+	{
+		super(location);
+	}
+
+    public void write(StringBuffer buffer, ScriptSession session)
+    {
+        if (buffer != null)
+            throw new IllegalArgumentException();
+
+        buffer = new StringBuffer(_bufferLengthHighwater);
+
+        writeChildren(buffer, session);
+
+		session.getProcessor().addBodyScript(buffer.toString());
+
+        // Store the buffer length from this run for the next run, since its
+        // going to be approximately the right size.
+
+        _bufferLengthHighwater = Math.max(_bufferLengthHighwater, buffer.length());
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/script/ForeachRule.java b/tapestry-framework/src/org/apache/tapestry/script/ForeachRule.java
new file mode 100644
index 0000000..e28c885
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/ForeachRule.java
@@ -0,0 +1,55 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.util.xml.RuleDirectedParser;
+import org.xml.sax.Attributes;
+
+/**
+ * Constructs a {@link org.apache.tapestry.script.ForeachToken}
+ * from a &lt;foreach&gt; element, which contains full content.
+ * 
+ * <p>As of 3.0, then index attribute has been added to foreach to keep 
+ * track of the current index of the iterating collection.</p>
+ *
+ * @author Howard Lewis Ship, Harish Krishnaswamy
+ * @version $Id$
+ * @since 3.0
+ */
+class ForeachRule extends AbstractTokenRule
+{
+
+    public void endElement(RuleDirectedParser parser)
+    {
+        parser.pop();
+    }
+
+    public void startElement(RuleDirectedParser parser, Attributes attributes)
+    {
+        String key = getAttribute(attributes, "key");
+        String index = getAttribute(attributes, "index");
+        String expression = getAttribute(attributes, "expression");
+
+        if (expression == null)
+            expression = getAttribute(attributes, "property-path"); // 1.0, 1.1 DTD
+
+        IScriptToken token = new ForeachToken(key, index, expression, parser.getLocation());
+
+        addToParent(parser, token);
+
+        parser.push(token);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/script/ForeachToken.java b/tapestry-framework/src/org/apache/tapestry/script/ForeachToken.java
new file mode 100644
index 0000000..472fcf9
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/ForeachToken.java
@@ -0,0 +1,82 @@
+//  Copyright 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.tapestry.script;
+
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  A looping operator, modeled after the Foreach component.  It takes
+ *  as its source as property and iterates through the values, updating
+ *  a symbol on each pass.
+ * 
+ *  <p>As of 3.0, the index attribute has been added to foreach to keep 
+ *  track of the current index of the iterating collection.</p>
+ *
+ *  @author Howard Lewis Ship, Harish Krishnaswamy
+ *  @version $Id$
+ *  @since 1.0.1
+ * 
+ **/
+
+class ForeachToken extends AbstractToken
+{
+    private String _key;
+    private String _index;
+    private String _expression;
+
+    ForeachToken(String key, String index, String expression, ILocation location)
+    {
+        super(location);
+
+        _key = key;
+        _index = index;
+        _expression = expression;
+    }
+
+    public void write(StringBuffer buffer, ScriptSession session)
+    {
+        Map symbols = session.getSymbols();
+
+        Object rawSource = evaluate(_expression, session);
+
+        Iterator i = Tapestry.coerceToIterator(rawSource);
+        
+        if (i == null)
+            return;
+
+        int index = 0;
+
+        while (i.hasNext())
+        {
+            Object newValue = i.next();
+
+            symbols.put(_key, newValue);
+            
+            if (_index != null)
+            	symbols.put(_index, String.valueOf(index));
+
+            writeChildren(buffer, session);
+            
+            index++;
+        }
+
+        // We leave the last value as a symbol; don't know if that's
+        // good or bad.
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/script/IScriptToken.java b/tapestry-framework/src/org/apache/tapestry/script/IScriptToken.java
new file mode 100644
index 0000000..a181da5
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/IScriptToken.java
@@ -0,0 +1,51 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.ILocatable;
+
+
+/**
+ *  Defines the responsibilities of a template token used by a
+ *  {@link org.apache.tapestry.IScript}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public interface IScriptToken extends ILocatable
+{
+	/**
+	 *  Invoked to have the token
+	 *  add its text to the buffer.  A token may need access
+	 *  to the symbols in order to produce its output.
+	 *
+	 *  <p>Top level tokens (such as BodyToken) can expect that
+	 *  buffer will be null.
+	 *
+	 **/
+
+	public void write(StringBuffer buffer, ScriptSession session);
+
+	/**
+	 *  Invoked during parsing to add the token parameter as a child
+	 *  of this token.
+	 *
+	 *  @since 0.2.9
+	 **/
+
+	public void addToken(IScriptToken token);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/script/IfRule.java b/tapestry-framework/src/org/apache/tapestry/script/IfRule.java
new file mode 100644
index 0000000..244d202
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/IfRule.java
@@ -0,0 +1,57 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.util.xml.RuleDirectedParser;
+import org.xml.sax.Attributes;
+
+/**
+ * Constructs an {@link org.apache.tapestry.script.IfToken}
+ * from an &lt;if&gt; or &lt;if-not&gt; element, which
+ * contains full content.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * @since 3.0
+ */
+class IfRule extends AbstractTokenRule
+{
+    private boolean _condition;
+
+    public IfRule(boolean condition)
+    {
+        _condition = condition;
+    }
+
+    public void endElement(RuleDirectedParser parser)
+    {
+        parser.pop();
+    }
+
+    public void startElement(RuleDirectedParser parser, Attributes attributes)
+    {
+        String expression = getAttribute(attributes, "expression");
+
+        if (expression == null)
+            expression = getAttribute(attributes, "property-path"); // 1.0, 1.1 DTD
+    
+    	IScriptToken token = new IfToken(_condition, expression, parser.getLocation());
+    	
+    	addToParent(parser, token);
+    	
+    	parser.push(token);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/script/IfToken.java b/tapestry-framework/src/org/apache/tapestry/script/IfToken.java
new file mode 100644
index 0000000..06ec384
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/IfToken.java
@@ -0,0 +1,54 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  A conditional portion of the generated script.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.1
+ *
+ **/
+
+class IfToken extends AbstractToken
+{
+    private boolean _condition;
+    private String _expression;
+
+    IfToken(boolean condition, String expression, ILocation location)
+    {
+        super(location);
+
+        _condition = condition;
+        _expression = expression;
+    }
+
+    private boolean evaluate(ScriptSession session)
+    {
+        Object value = evaluate(_expression, session);
+
+        return Tapestry.evaluateBoolean(value);
+    }
+
+    public void write(StringBuffer buffer, ScriptSession session)
+    {
+        if (evaluate(session) == _condition)
+            writeChildren(buffer, session);
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/script/IncludeScriptRule.java b/tapestry-framework/src/org/apache/tapestry/script/IncludeScriptRule.java
new file mode 100644
index 0000000..02c74f2
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/IncludeScriptRule.java
@@ -0,0 +1,42 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.util.xml.BaseRule;
+import org.apache.tapestry.util.xml.RuleDirectedParser;
+import org.xml.sax.Attributes;
+
+/**
+ * Constructs an {@link org.apache.tapestry.script.IncludeScriptToken}
+ * from a &lt;include-script&gt; element, which contains no content.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * @since 3.0
+ */
+class IncludeScriptRule extends BaseRule
+{
+
+    public void startElement(RuleDirectedParser parser, Attributes attributes)
+    {
+        String path = getAttribute(attributes, "resource-path");
+        
+        IncludeScriptToken token = new IncludeScriptToken(path, parser.getLocation());
+
+		IScriptToken parent = (IScriptToken) parser.peek();
+        parent.addToken(token);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/script/IncludeScriptToken.java b/tapestry-framework/src/org/apache/tapestry/script/IncludeScriptToken.java
new file mode 100644
index 0000000..49a694c
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/IncludeScriptToken.java
@@ -0,0 +1,63 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.resource.ClasspathResourceLocation;
+
+/**
+ *  A token for included scripts.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.5
+ * 
+ **/
+
+class IncludeScriptToken extends AbstractToken
+{
+    private String _resourcePath;
+
+    public IncludeScriptToken(String resourcePath, ILocation location)
+    {
+        super(location);
+
+        _resourcePath = resourcePath;
+    }
+
+    public void write(StringBuffer buffer, ScriptSession session)
+    {
+        IResourceLocation includeLocation = null;
+
+        if (_resourcePath.startsWith("/"))
+        {
+            includeLocation =
+                new ClasspathResourceLocation(
+                    session.getRequestCycle().getEngine().getResourceResolver(),
+                    _resourcePath);
+        }
+        else
+        {
+            IResourceLocation baseLocation = session.getScriptPath();
+            includeLocation = baseLocation.getRelativeLocation(_resourcePath);
+        }
+
+        // TODO: Allow for scripts relative to context resources!
+
+        session.getProcessor().addExternalScript(includeLocation);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/script/InitRule.java b/tapestry-framework/src/org/apache/tapestry/script/InitRule.java
new file mode 100644
index 0000000..8872f99
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/InitRule.java
@@ -0,0 +1,46 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.util.xml.RuleDirectedParser;
+import org.xml.sax.Attributes;
+
+/**
+ * Constructs an {@link org.apache.tapestry.script.InitToken}
+ * from an &lt;initialization&gt; element, which
+ * contains full content.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * @since 3.0
+ */
+public class InitRule extends AbstractTokenRule
+{
+
+    public void endElement(RuleDirectedParser parser)
+    {
+		parser.pop();
+    }
+
+    public void startElement(RuleDirectedParser parser, Attributes attributes)
+    {
+		IScriptToken token = new InitToken(parser.getLocation());
+		
+		addToParent(parser, token);
+		
+		parser.push(token);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/script/InitToken.java b/tapestry-framework/src/org/apache/tapestry/script/InitToken.java
new file mode 100644
index 0000000..882a995
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/InitToken.java
@@ -0,0 +1,54 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.ILocation;
+
+/**
+ *  Generates a String from its child tokens, then applies it
+ *  to {@link ScriptSession#setInitialization(String)}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 0.2.9
+ *
+ **/
+
+class InitToken extends AbstractToken
+{
+    private int _bufferLengthHighwater = 100;
+
+    public InitToken(ILocation location)
+    {
+        super(location);
+    }
+
+    public void write(StringBuffer buffer, ScriptSession session)
+    {
+        if (buffer != null)
+            throw new IllegalArgumentException();
+
+        buffer = new StringBuffer(_bufferLengthHighwater);
+
+        writeChildren(buffer, session);
+
+        session.getProcessor().addInitializationScript(buffer.toString());
+
+        // Store the buffer length from this run for the next run, since its
+        // going to be approximately the right size.
+
+        _bufferLengthHighwater = Math.max(_bufferLengthHighwater, buffer.length());
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/script/InputSymbolRule.java b/tapestry-framework/src/org/apache/tapestry/script/InputSymbolRule.java
new file mode 100644
index 0000000..042f4f3
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/InputSymbolRule.java
@@ -0,0 +1,77 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.util.xml.BaseRule;
+import org.apache.tapestry.util.xml.DocumentParseException;
+import org.apache.tapestry.util.xml.RuleDirectedParser;
+import org.xml.sax.Attributes;
+
+/**
+ * Constructs an {@link org.apache.tapestry.script.InputSymbolToken}
+ * from an &lt;input-symbol&gt; element.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * @since 3.0
+ */
+class InputSymbolRule extends BaseRule
+{
+    private IResourceResolver _resolver;
+
+    public InputSymbolRule(IResourceResolver resolver)
+    {
+        _resolver = resolver;
+    }
+
+    public void startElement(RuleDirectedParser parser, Attributes attributes)
+    {
+        String key = getAttribute(attributes, "key");
+
+        parser.validate(key, Tapestry.SIMPLE_PROPERTY_NAME_PATTERN, "ScriptParser.invalid-key");
+
+        String className = getAttribute(attributes, "class");
+        Class expectedClass = lookupClass(parser, className);
+
+        String required = getAttribute(attributes, "required");
+
+        InputSymbolToken token =
+            new InputSymbolToken(key, expectedClass, required.equals("yes"), parser.getLocation());
+
+        IScriptToken parent = (IScriptToken) parser.peek();
+        parent.addToken(token);
+    }
+
+    private Class lookupClass(RuleDirectedParser parser, String className)
+    {
+        if (Tapestry.isBlank(className))
+            return null;
+
+        try
+        {
+            return _resolver.findClass(className);
+        }
+        catch (Exception ex)
+        {
+            throw new DocumentParseException(
+                Tapestry.format("ScriptParser.unable-to-resolve-class", className),
+                parser.getDocumentLocation(),
+                parser.getLocation(),
+                ex);
+        }
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/script/InputSymbolToken.java b/tapestry-framework/src/org/apache/tapestry/script/InputSymbolToken.java
new file mode 100644
index 0000000..dbecfd6
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/InputSymbolToken.java
@@ -0,0 +1,68 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  A token that validates that an input symbol exists or is of a
+ *  declared type.
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.2
+ * 
+ **/
+
+class InputSymbolToken extends AbstractToken
+{
+    private String _key;
+    private Class _class;
+    private boolean _required;
+
+    InputSymbolToken(String key, Class clazz, boolean required, ILocation location)
+    {
+        super(location);
+
+        _key = key;
+        _class = clazz;
+        _required = required;
+    }
+
+    public void write(StringBuffer buffer, ScriptSession session)
+    {
+        Object value = session.getSymbols().get(_key);
+
+        if (_required && value == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.format("InputSymbolToken.required", _key),
+                getLocation(),
+                null);
+
+        if (value != null && _class != null && !_class.isAssignableFrom(value.getClass()))
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "InputSymbolToken.wrong-type",
+                    _key,
+                    value.getClass().getName(),
+                    _class.getName()),
+                getLocation(),
+                null);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/script/InsertRule.java b/tapestry-framework/src/org/apache/tapestry/script/InsertRule.java
new file mode 100644
index 0000000..c3b919f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/InsertRule.java
@@ -0,0 +1,53 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.util.xml.RuleDirectedParser;
+import org.xml.sax.Attributes;
+
+/**
+ * Constructs an {@link org.apache.tapestry.script.InsertToken}
+ * from an &lt;insert&gt; element, which contains full content.
+ * &lt;insert&gt; is a throwback to the 1.0 and 1.1 DTDs.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * @since 3.0
+ */
+class InsertRule extends AbstractTokenRule
+{
+
+    public void endElement(RuleDirectedParser parser)
+    {
+        parser.pop();
+    }
+
+    public void startElement(RuleDirectedParser parser, Attributes attributes)
+    {
+        // property-path is really an OGNL expression.
+        String expression = getAttribute(attributes, "property-path");
+
+        // Was called key in the 1.0 DTD
+        if (expression == null)
+            expression = getAttribute(attributes, "key");
+
+        IScriptToken token = new InsertToken(expression, parser.getLocation());
+
+        addToParent(parser, token);
+
+        parser.push(token);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/script/InsertToken.java b/tapestry-framework/src/org/apache/tapestry/script/InsertToken.java
new file mode 100644
index 0000000..fb34dc7
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/InsertToken.java
@@ -0,0 +1,58 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.ILocation;
+
+/**
+ *  A token that writes the value of a property using a property path
+ *  routed in the symbols..
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+class InsertToken extends AbstractToken
+{
+    private String _expression;
+
+    InsertToken(String expression, ILocation location)
+    {
+        super(location);
+
+        _expression = expression;
+    }
+
+    /**
+     *  Gets the named symbol from the symbols {@link Map}, verifies that
+     *  it is a String, and writes it to the {@link Writer}.
+     *
+     **/
+
+    public void write(StringBuffer buffer, ScriptSession session)
+    {
+        Object value = evaluate(_expression, session);
+
+        if (value != null)
+            buffer.append(value);
+    }
+
+    public void addToken(IScriptToken token)
+    {
+        // Should never be invoked.
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/script/LetRule.java b/tapestry-framework/src/org/apache/tapestry/script/LetRule.java
new file mode 100644
index 0000000..63aef50
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/LetRule.java
@@ -0,0 +1,53 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.util.xml.RuleDirectedParser;
+import org.xml.sax.Attributes;
+
+/**
+ * Constructs an {@link org.apache.tapestry.script.LetToken}
+ * from a &lt;let&gt; element, which may contain full content.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * @since 3.0
+ */
+class LetRule extends AbstractTokenRule
+{
+
+    public void startElement(RuleDirectedParser parser, Attributes attributes)
+    {
+        String key = getAttribute(attributes, "key");
+        
+        String unique = getAttribute(attributes, "unique");
+        boolean uniqueFlag = unique != null && unique.equals("yes"); 
+
+        parser.validate(key, Tapestry.SIMPLE_PROPERTY_NAME_PATTERN, "ScriptParser.invalid-key");
+
+        LetToken token = new LetToken(key, uniqueFlag, parser.getLocation());
+
+        addToParent(parser, token);
+
+        parser.push(token);
+    }
+
+    public void endElement(RuleDirectedParser parser)
+    {
+        parser.pop();
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/script/LetToken.java b/tapestry-framework/src/org/apache/tapestry/script/LetToken.java
new file mode 100644
index 0000000..8561ec4
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/LetToken.java
@@ -0,0 +1,73 @@
+//  Copyright 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.tapestry.script;
+
+import java.util.Map;
+
+import org.apache.tapestry.ILocation;
+
+/**
+ *  Allows for the creation of new symbols that can be used in the script
+ *  or returned to the caller.
+ *
+ *  <p>The &lt;let&gt; tag wraps around static text and &lt;insert&gt;
+ *  elements.  The results are trimmed.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 0.2.9
+ * 
+ **/
+
+class LetToken extends AbstractToken
+{
+    private String _key;
+    private boolean _unique;
+    private int _bufferLengthHighwater = 20;
+
+    public LetToken(String key, boolean unique, ILocation location)
+    {
+        super(location);
+
+        _key = key;
+        _unique = unique;
+    }
+
+    public void write(StringBuffer buffer, ScriptSession session)
+    {
+        if (buffer != null)
+            throw new IllegalArgumentException();
+
+        buffer = new StringBuffer(_bufferLengthHighwater);
+
+        writeChildren(buffer, session);
+
+        // Store the symbol back into the root set of symbols.
+
+        Map symbols = session.getSymbols();
+
+        String value = buffer.toString().trim();
+
+        if (_unique)
+            value = session.getProcessor().getUniqueString(value);
+
+        symbols.put(_key, value);
+
+        // Store the buffer length from this run for the next run, since its
+        // going to be approximately the right size.
+
+        _bufferLengthHighwater = Math.max(_bufferLengthHighwater, buffer.length());
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/script/ParsedScript.java b/tapestry-framework/src/org/apache/tapestry/script/ParsedScript.java
new file mode 100644
index 0000000..94e7036
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/ParsedScript.java
@@ -0,0 +1,68 @@
+//  Copyright 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.tapestry.script;
+
+import java.util.Map;
+
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.IScript;
+import org.apache.tapestry.IScriptProcessor;
+
+/**
+ *  A top level container for a number of {@link IScriptToken script tokens}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 0.2.9
+ * 
+ **/
+
+public class ParsedScript extends AbstractToken implements IScript
+{
+    private IResourceLocation _scriptLocation;
+
+    public ParsedScript(ILocation location)
+    {
+ 		super(location);
+ 		
+ 		_scriptLocation = location.getResourceLocation();
+    }
+
+    public IResourceLocation getScriptLocation()
+    {
+        return _scriptLocation;
+    }
+
+	/**
+	 * Creates the {@link ScriptSession} and invokes 
+	 * {@link org.apache.tapestry.script.AbstractToken#writeChildren(java.lang.StringBuffer, org.apache.tapestry.script.ScriptSession)}.
+	 */
+    public void execute(IRequestCycle cycle, IScriptProcessor processor, Map symbols)
+    {
+        ScriptSession session = new ScriptSession(_scriptLocation, cycle, processor, symbols);
+		writeChildren(null, session);
+    }
+    
+    /** 
+     * Does nothing; never invoked. 
+     */
+    public void write(StringBuffer buffer, ScriptSession session)
+    {
+
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/script/ScriptParser.java b/tapestry-framework/src/org/apache/tapestry/script/ScriptParser.java
new file mode 100644
index 0000000..5efe0de
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/ScriptParser.java
@@ -0,0 +1,118 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.IScript;
+import org.apache.tapestry.util.xml.DocumentParseException;
+import org.apache.tapestry.util.xml.RuleDirectedParser;
+
+/**
+ *  Parses a Tapestry Script, an XML file defined by one of the following
+ *  public identifiers:
+ *  <ul>
+ *  <li><code>-//Primix Solutions//Tapestry Script 1.0//EN</code></li> 
+ *  <li><code>-//Howard Ship//Tapestry Script 1.1//EN</code></li>
+ *  <li><code>-//Howard Lewis Ship//Tapestry Script 1.2//EN</code></li>
+ * .
+ *
+ *  <p>The version 1.1, is largely backwards compatible to the
+ *  old script, but adds a number of new features (if, if-not, foreach
+ *  and the use of property paths with insert).
+ * 
+ *  <p>Version 1.2 removes the &lt;insert&gt; element, using an Ant-like
+ *  syntax (<code>${<i>expression</i>}</code>).  It also replaces
+ *  the attribute name <code>property-path</code> with <code>expression</code>
+ *  (because OGNL is used).
+ *
+ *  <p>A Tapestry Script is used, in association with the 
+ *  {@link org.apache.tapestry.html.Body} and/or 
+ *  {@link org.apache.tapestry.html.Script} components,
+ *  to generate JavaScript for use with a Tapestry component.  Two seperate pieces
+ *  of JavaScript can be generated.  The body section (associated with the <code>body</code>
+ *  element of the XML document) is typically used to define JavaScript functions
+ *  (most often, event handlers).  The initialization section
+ *  (associated with the <code>initialization</code> element of the XML document)
+ *  is used to add JavaScript that will be evaluated when the page finishes loading
+ *  (i.e., from the HTML &lt;body&gt; element's onLoad event handler).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class ScriptParser
+{
+    private RuleDirectedParser _parser;
+
+    public static final String SCRIPT_DTD_1_0_PUBLIC_ID =
+        "-//Primix Solutions//Tapestry Script 1.0//EN";
+
+    public static final String SCRIPT_DTD_1_1_PUBLIC_ID = "-//Howard Ship//Tapestry Script 1.1//EN";
+
+    public static final String SCRIPT_DTD_1_2_PUBLIC_ID =
+        "-//Howard Lewis Ship//Tapestry Script 1.2//EN";
+
+    /** @since 3.0 */
+    public static final String SCRIPT_DTD_3_0_PUBLIC_ID =
+        "-//Apache Software Foundation//Tapestry Script Specification 3.0//EN";
+
+    public ScriptParser(IResourceResolver resolver)
+    {
+        _parser = new RuleDirectedParser();
+
+        _parser.registerEntity(
+            SCRIPT_DTD_1_0_PUBLIC_ID,
+            "/org/apache/tapestry/script/Script_1_0.dtd");
+        _parser.registerEntity(
+            SCRIPT_DTD_1_1_PUBLIC_ID,
+            "/org/apache/tapestry/script/Script_1_1.dtd");
+        _parser.registerEntity(
+            SCRIPT_DTD_1_2_PUBLIC_ID,
+            "/org/apache/tapestry/script/Script_1_2.dtd");
+        _parser.registerEntity(
+            SCRIPT_DTD_3_0_PUBLIC_ID,
+            "/org/apache/tapestry/script/Script_3_0.dtd");
+
+        _parser.addRule("script", new ScriptRule());
+        _parser.addRule("let", new LetRule());
+        _parser.addRule("set", new SetRule());
+        _parser.addRule("include-script", new IncludeScriptRule());
+        _parser.addRule("input-symbol", new InputSymbolRule(resolver));
+        _parser.addRule("body", new BodyRule());
+        _parser.addRule("initialization", new InitRule());
+        _parser.addRule("if", new IfRule(true));
+        _parser.addRule("if-not", new IfRule(false));
+        _parser.addRule("foreach", new ForeachRule());
+        _parser.addRule("unique", new UniqueRule());
+
+        // This will go away when the 1.1 and earler DTDs are retired.
+        _parser.addRule("insert", new InsertRule());
+
+    }
+
+    /**
+     *  Parses the given input stream to produce a parsed script,
+     *  ready to execute.
+     *
+     **/
+
+    public IScript parse(IResourceLocation resourceLocation) throws DocumentParseException
+    {
+        return (IScript) _parser.parse(resourceLocation);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/script/ScriptRule.java b/tapestry-framework/src/org/apache/tapestry/script/ScriptRule.java
new file mode 100644
index 0000000..7eccad8
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/ScriptRule.java
@@ -0,0 +1,43 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.util.xml.BaseRule;
+import org.apache.tapestry.util.xml.RuleDirectedParser;
+import org.xml.sax.Attributes;
+
+/**
+ * Rule for &lt;script&gt; element.  Creates a {@link org.apache.tapestry.script.ParsedScript}.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * @since 3.0
+ */
+public class ScriptRule extends BaseRule
+{
+
+    public void endElement(RuleDirectedParser parser)
+    {
+        parser.pop();
+    }
+
+    public void startElement(RuleDirectedParser parser, Attributes attributes)
+    {
+        ParsedScript script = new ParsedScript(parser.getLocation());
+
+        parser.push(script);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/script/ScriptSession.java b/tapestry-framework/src/org/apache/tapestry/script/ScriptSession.java
new file mode 100644
index 0000000..a8dae06
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/ScriptSession.java
@@ -0,0 +1,84 @@
+//  Copyright 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.tapestry.script;
+
+import java.util.Map;
+
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.IScriptProcessor;
+
+/**
+ *  The result of executing a script, the session is used during the parsing
+ *  process as well.  Following {@link org.apache.tapestry.IScript#execute(org.apache.tapestry.IRequestCycle, org.apache.tapestry.IScriptProcessor, java.util.Map)}, the session
+ *  provides access to output symbols as well as the body and initialization
+ *  blocks created by the script tokens.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 0.2.9
+ * 
+ **/
+
+public class ScriptSession
+{
+    private IRequestCycle _cycle;
+    private IScriptProcessor _processor;
+    private IResourceLocation _scriptLocation;
+    private Map _symbols;
+
+    public ScriptSession(
+        IResourceLocation scriptLocation,
+        IRequestCycle cycle,
+        IScriptProcessor processor,
+        Map symbols)
+    {
+        _scriptLocation = scriptLocation;
+        _cycle = cycle;
+        _processor = processor;
+        _symbols = symbols;
+    }
+
+    public IResourceLocation getScriptPath()
+    {
+        return _scriptLocation;
+    }
+
+    public Map getSymbols()
+    {
+        return _symbols;
+    }
+
+	public IRequestCycle getRequestCycle()
+	{
+		return _cycle;
+	}
+
+    public IScriptProcessor getProcessor()
+    {
+        return _processor;
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer();
+
+        buffer.append("ScriptSession[");
+        buffer.append(_scriptLocation);
+        buffer.append(']');
+
+        return buffer.toString();
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/script/Script_1_0.dtd b/tapestry-framework/src/org/apache/tapestry/script/Script_1_0.dtd
new file mode 100644
index 0000000..5c91d5d
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/Script_1_0.dtd
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<!-- $Id: Script_1_0.dtd,v 1.3 2002/05/02 17:52:39 hship Exp $ -->

+<!--DTD for the files used with the ScriptGenerator class and Script component.  This is recognized with the public identifier:

+

+	-//Primix Solutions//Tapestry Script 1.0//EN

+

+-->

+

+<!--

+

+Element: script

+

+Root element.

+

+-->

+

+<!ELEMENT script (let*, body?, initialization?)>

+

+<!--

+

+Element: let

+Contained by: script

+

+Used to create a new symbol.

+

+-->

+

+<!ELEMENT let (#PCDATA | insert)*>

+<!ATTLIST let

+	key CDATA #REQUIRED

+>

+

+<!--

+

+Element: body

+Contained by: script

+

+Allows a mix of text and insert elements.  This text is added to

+the large scripting block just before the <body> tag.

+-->

+

+<!ELEMENT body (#PCDATA | insert)*>

+

+<!--

+Element: initialization

+Contained by: script

+

+Text in this block is added to the event handler for the <body>

+tag's onLoad event.

+-->

+

+<!ELEMENT initialization (#PCDATA | insert)*>

+

+<!--

+Element: insert

+Contained by: body, initialization

+

+Allows an arbitrary symbol to be inserted.

+-->

+

+<!ELEMENT insert (#PCDATA)>

+<!ATTLIST insert

+	key CDATA #REQUIRED

+>

diff --git a/tapestry-framework/src/org/apache/tapestry/script/Script_1_1.dtd b/tapestry-framework/src/org/apache/tapestry/script/Script_1_1.dtd
new file mode 100644
index 0000000..911624c
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/Script_1_1.dtd
@@ -0,0 +1,137 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<!-- $Id: Script_1_1.dtd,v 1.3 2002/05/02 17:52:39 hship Exp $ -->

+<!--

+

+DTD for the files used with the ScriptGenerator class and Script component.  

+This is recognized with the public identifier:

+

+	-//Howard Ship//Tapestry Script 1.1//EN

+

+The canonical location for the DTD is:

+

+	http://tapestry.sf.net/dtd/Tapestry_1_1.dtd

+

+The root element is always script.

+	

+-->

+<!-- =======================================================

+

+Entity: full-content

+

+Identifies the contents of most of the other elements.

+

+-->

+<!ENTITY % full-content "(#PCDATA | foreach | insert | if | if-not)*">

+<!-- =======================================================

+

+Element: body

+Contained by: script

+

+Allows a mix of text and insert elements.  This text is added to

+the large scripting block just before the <body> tag.

+-->

+<!ELEMENT body %full-content;>

+<!-- =======================================================

+

+Element: foreach

+Appears in: %full-content;

+

+Iterates over a list of items; this is modeled after the

+Foreach component.  No iteration occurs if the value

+from the property path is null.

+

+Attributes:

+  key: Defines the symbol into which each succesive value is stored.

+  property-path: The source of values.

+-->

+<!ELEMENT foreach %full-content;>

+<!ATTLIST foreach

+  key CDATA #REQUIRED

+  property-path CDATA #REQUIRED

+>

+

+<!-- =======================================================

+Element: include-script

+Contained by: script

+

+Inserts a reference to an external, static, JavaScript file.

+

+Attributes:

+  resource-path: The path to the script within the classpath.

+-->

+<!ELEMENT include-script EMPTY>

+<!ATTLIST include-script

+  resource-path CDATA #REQUIRED

+>

+

+<!-- =======================================================

+

+Element: if

+Appears in: %full-content;

+

+Creates a conditional portion of the script; The body of the element 

+is only included if the property-path evaulates to true.

+

+-->

+<!ELEMENT if %full-content;>

+<!ATTLIST if

+  property-path CDATA #REQUIRED

+>

+<!-- =======================================================

+

+Element: if-not

+Appears in: %full-content;

+

+Creates a conditional portion of the script; The body of the element 

+is only included if the property-path evaulates to false.

+

+-->

+<!ELEMENT if-not %full-content;>

+<!ATTLIST if-not

+  property-path CDATA #REQUIRED

+>

+<!-- =======================================================

+Element: initialization

+Contained by: script

+

+Text in this block is added to the event handler for the <body>

+tag's onLoad event.

+-->

+<!ELEMENT initialization %full-content;>

+<!-- =======================================================

+Element: insert

+Contained by: body, initialization

+

+Allows an arbitrary symbol to be inserted.

+

+Attributes:

+  property-path: The path to the value to insert.

+-->

+<!ELEMENT insert EMPTY>

+<!ATTLIST insert

+  property-path CDATA #REQUIRED

+>

+

+<!-- =======================================================

+

+Element: let

+Contained by: script

+

+Used to create a new symbol.

+

+-->

+<!ELEMENT let %full-content;>

+<!ATTLIST let

+  key CDATA #REQUIRED

+>

+<!-- =======================================================

+

+Element: script

+

+Root element.

+

+Allows zero or more let elements (to establish new symbols),

+followed by a body and/or initialization element.

+

+-->

+<!ELEMENT script (include-script*, let*, body?, initialization?)>

diff --git a/tapestry-framework/src/org/apache/tapestry/script/Script_1_2.dtd b/tapestry-framework/src/org/apache/tapestry/script/Script_1_2.dtd
new file mode 100644
index 0000000..52ef476
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/Script_1_2.dtd
@@ -0,0 +1,168 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<!-- $Id: Script_1_2.dtd,v 1.3 2002/09/16 19:33:02 hship Exp $ -->

+<!--

+

+DTD for the files used with the ScriptParser class and Script component.  

+This is recognized with the public identifier:

+

+	-//Howard Lewis Ship//Tapestry Script 1.2//EN

+

+The canonical location for the DTD is:

+

+	http://tapestry.sf.net/dtd/Script_1_2.dtd

+

+The root element is always script.

+	

+-->

+<!-- =======================================================

+

+Entity: full-content

+

+Identifies the contents of most of the other elements.

+

+-->

+<!ENTITY % full-content "(#PCDATA | foreach | if | if-not)*">

+<!-- =======================================================

+

+Element: body

+Contained by: script

+

+Allows a mix of text and insert elements.  This text is added to

+the large scripting block just before the <body> tag.

+-->

+<!ELEMENT body %full-content;>

+<!-- =======================================================

+

+Element: foreach

+Appears in: %full-content;

+

+Iterates over a list of items; this is modeled after the

+Foreach component.  No iteration occurs if the value

+from the property path is null.

+

+Attributes:

+  key: Defines the symbol into which each succesive value is stored.

+  expression: The source of values, as an OGNL expression rooted in the symbols Map.

+-->

+<!ELEMENT foreach %full-content;>

+<!ATTLIST foreach

+  key CDATA #REQUIRED

+  expression CDATA #REQUIRED

+>

+

+<!-- =======================================================

+Element: include-script

+Contained by: script

+

+Inserts a reference to an external, static, JavaScript file.

+

+Attributes:

+  resource-path: The path to the script within the classpath.

+-->

+<!ELEMENT include-script EMPTY>

+<!ATTLIST include-script

+  resource-path CDATA #REQUIRED

+>

+

+<!-- =======================================================

+

+Element: if

+Appears in: %full-content;

+

+Creates a conditional portion of the script; The body of the element 

+is only included if the expression evaulates to true.

+

+Attributes:

+  expression: The trigger expression, as an OGNL expression rooted in 

+    the symbols Map.

+

+-->

+<!ELEMENT if %full-content;>

+<!ATTLIST if

+  expression CDATA #REQUIRED

+>

+<!-- =======================================================

+

+Element: if-not

+Appears in: %full-content;

+

+Creates a conditional portion of the script; The body of the element 

+is only included if the property-path evaulates to false.

+

+Attributes:

+  expression: The trigger expression, as an OGNL expression rooted in 

+    the symbols Map.

+

+-->

+<!ELEMENT if-not %full-content;>

+<!ATTLIST if-not

+  expression CDATA #REQUIRED

+>

+<!-- =======================================================

+Element: initialization

+Contained by: script

+

+Text in this block is added to the event handler for the <body>

+tag's onLoad event.

+-->

+<!ELEMENT initialization %full-content;>

+

+

+

+<!-- =======================================================

+Element: input-symbol

+Contained by: script

+

+Defines an input symbol used by the script.

+Attributes:

+  key: The name of the symbol.

+  class:  If specified, the exected class or interface for the symbol.

+  required: If yes, then the symbol must be non-null.

+-->

+

+<!ELEMENT input-symbol EMPTY>

+<!ATTLIST input-symbol

+  key CDATA #REQUIRED

+  class CDATA #IMPLIED

+  required (yes|no) "no"

+>

+

+<!-- =======================================================

+

+Element: let

+Contained by: script

+

+Used to create a new symbol.

+

+-->

+<!ELEMENT let %full-content;>

+<!ATTLIST let

+  key CDATA #REQUIRED

+>

+<!-- =======================================================

+

+Element: script

+

+Root element.

+

+Allows zero or more let elements (to establish new symbols),

+followed by a body and/or initialization element.

+

+-->

+<!ELEMENT script (include-script*, input-symbol*, 

+	(let | set)*, body?, initialization?)>

+

+

+<!-- =======================================================

+

+Element: set

+Contained by: script

+

+Creates a new symbol as the result of evaluating an OGNL expression.

+

+-->

+<!ELEMENT set EMPTY>

+<!ATTLIST set

+  key CDATA #REQUIRED

+  expression CDATA #REQUIRED

+>
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/script/Script_3_0.dtd b/tapestry-framework/src/org/apache/tapestry/script/Script_3_0.dtd
new file mode 100644
index 0000000..e36a857
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/Script_3_0.dtd
@@ -0,0 +1,197 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<!-- $Id: Script_1_2.dtd,v 1.3 2002/09/16 19:33:02 hship Exp $ -->

+<!--

+

+DTD for the files used with the ScriptParser class and Script component.  

+This is recognized with the public identifier:

+

+	-//Apache Software Foundation//Tapestry Script Specification 1.3//EN

+

+The canonical location for the DTD is:

+

+	http://jakarta.apache.org/tapestry/dtd/Script_1_3.dtd

+

+The root element is always script.

+	

+This DTD is backwards compatible with the 1.2 DTD, with the following exceptions:

+- Addition of <unique> element

+- Addition of unique attribute to <let> element

+- Addition of index attribute to <foreach> element

+-->

+<!-- =======================================================

+

+Entity: full-content

+

+Identifies the contents of most of the other elements.

+

+-->

+<!ENTITY % full-content "(#PCDATA | foreach | if | if-not | unique)*">

+<!-- =======================================================

+

+Element: body

+Contained by: script

+

+Allows a mix of text and insert elements.  This text is added to

+the large scripting block just before the <body> tag.

+-->

+<!ELEMENT body %full-content;>

+<!-- =======================================================

+

+Element: foreach

+Appears in: %full-content;

+

+Iterates over a list of items; this is modeled after the

+Foreach component.  No iteration occurs if the value

+from the property path is null.

+

+Attributes:

+  key: Defines the symbol into which each succesive value is stored.

+  index: Defines the symbol into which the index of the value of the current iteration is stored.

+  expression: The source of values, as an OGNL expression rooted in the symbols Map.

+-->

+<!ELEMENT foreach %full-content;>

+<!ATTLIST foreach

+  key CDATA #IMPLIED

+  index CDATA #IMPLIED

+  expression CDATA #REQUIRED

+>

+

+<!-- =======================================================

+Element: include-script

+Contained by: script

+

+Inserts a reference to an external, static, JavaScript file.

+

+Attributes:

+  resource-path: The path to the script within the classpath.

+-->

+<!ELEMENT include-script EMPTY>

+<!ATTLIST include-script

+  resource-path CDATA #REQUIRED

+>

+

+<!-- =======================================================

+

+Element: if

+Appears in: %full-content;

+

+Creates a conditional portion of the script; The body of the element 

+is only included if the expression evaulates to true.

+

+Attributes:

+  expression: The trigger expression, as an OGNL expression rooted in 

+    the symbols Map.

+

+-->

+<!ELEMENT if %full-content;>

+<!ATTLIST if

+  expression CDATA #REQUIRED

+>

+<!-- =======================================================

+

+Element: if-not

+Appears in: %full-content;

+

+Creates a conditional portion of the script; The body of the element 

+is only included if the property-path evaulates to false.

+

+Attributes:

+  expression: The trigger expression, as an OGNL expression rooted in 

+    the symbols Map.

+

+-->

+<!ELEMENT if-not %full-content;>

+<!ATTLIST if-not

+  expression CDATA #REQUIRED

+>

+<!-- =======================================================

+Element: initialization

+Contained by: script

+

+Text in this block is added to the event handler for the <body>

+tag's onLoad event.

+-->

+<!ELEMENT initialization %full-content;>

+

+

+

+<!-- =======================================================

+Element: input-symbol

+Contained by: script

+

+Defines an input symbol used by the script.

+Attributes:

+  key: The name of the symbol.

+  class:  If specified, the exected class or interface for the symbol.

+  required: If yes, then the symbol must be non-null.

+-->

+

+<!ELEMENT input-symbol EMPTY>

+<!ATTLIST input-symbol

+  key CDATA #REQUIRED

+  class CDATA #IMPLIED

+  required (yes|no) "no"

+>

+

+<!-- =======================================================

+

+Element: let

+Contained by: script

+

+Used to create a new symbol. The content of the tag

+is used to create a string that is the name.  If the

+unique flag is enabled, the name is ensured to be unique

+(a suffix may be appended to ensure it is unique

+among all names so generated).

+

+Attributes:

+  key: The name of the symbol to create.

+  unique: If yes, the name is ensured to be unique.

+  The default is no.

+

+-->

+<!ELEMENT let %full-content;>

+<!ATTLIST let

+  key CDATA #REQUIRED

+  unique (yes|no) "no"

+>

+<!-- =======================================================

+

+Element: script

+

+Root element.

+

+Allows zero or more let elements (to establish new symbols),

+followed by a body and/or initialization element.

+

+-->

+<!ELEMENT script (include-script*, input-symbol*, 

+	(let | set)*, body?, initialization?)>

+

+

+<!-- =======================================================

+

+Element: set

+Contained by: script

+

+Creates a new symbol as the result of evaluating an OGNL expression.

+

+-->

+<!ELEMENT set EMPTY>

+<!ATTLIST set

+  key CDATA #REQUIRED

+  expression CDATA #REQUIRED

+>

+

+<!-- =======================================================

+

+Element: unique

+Appears in: %full-content;

+

+Defines a block that only is rendered once per page.

+This is appropriate to certain kinds of initialization code

+that should not be duplicated, even if the script is

+executed multiple times.

+

+-->

+<!ELEMENT unique %full-content;>

diff --git a/tapestry-framework/src/org/apache/tapestry/script/SetRule.java b/tapestry-framework/src/org/apache/tapestry/script/SetRule.java
new file mode 100644
index 0000000..8de003c
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/SetRule.java
@@ -0,0 +1,53 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.util.xml.BaseRule;
+import org.apache.tapestry.util.xml.RuleDirectedParser;
+import org.xml.sax.Attributes;
+
+/**
+ * Constructs at {@link org.apache.tapestry.script.SetToken} from at &lt;set&gt; element,
+ * which contains full content.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * @since 3.0
+ */
+
+class SetRule extends BaseRule
+{
+    public void startElement(RuleDirectedParser parser, Attributes attributes)
+    {
+        String key = getAttribute(attributes, "key");
+
+        parser.validate(key, Tapestry.SIMPLE_PROPERTY_NAME_PATTERN, "ScriptParser.invalid-key");
+
+        String expression = getAttribute(attributes, "expression");
+
+        SetToken token = new SetToken(key, expression, parser.getLocation());
+
+        IScriptToken parent = (IScriptToken) parser.peek();
+        parent.addToken(token);
+
+        parser.push(token);
+    }
+
+    public void endElement(RuleDirectedParser parser)
+    {
+        parser.pop();
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/script/SetToken.java b/tapestry-framework/src/org/apache/tapestry/script/SetToken.java
new file mode 100644
index 0000000..52038ef
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/SetToken.java
@@ -0,0 +1,57 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.ILocation;
+
+/**
+ *  
+ *  Like {@link org.apache.tapestry.script.LetToken}, but sets the value
+ *  from an expression attribute, rather than a body of full content.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.2
+ *
+ **/
+
+class SetToken extends AbstractToken
+{
+    private String _key;
+    private String _expression;
+
+    SetToken(String key, String expression, ILocation location)
+    {
+        super(location);
+        _key = key;
+        _expression = expression;
+    }
+
+    /**
+     *   
+     *  Doesn't <em>write</em>, it evaluates the expression and assigns
+     *  the result back to the key. 
+     * 
+     **/
+
+    public void write(StringBuffer buffer, ScriptSession session)
+    {
+
+        Object value = evaluate(_expression, session);
+
+        session.getSymbols().put(_key, value);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/script/StaticToken.java b/tapestry-framework/src/org/apache/tapestry/script/StaticToken.java
new file mode 100644
index 0000000..9a1b525
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/StaticToken.java
@@ -0,0 +1,53 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.ILocation;
+
+
+/**
+ *  A token for static portions of the template.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+class StaticToken extends AbstractToken
+{
+    private String _text;
+
+    StaticToken(String text, ILocation location	)
+    {
+    	super(location);
+    	
+        _text = text;
+    }
+
+    /**
+     *  Writes the text to the writer.
+     *
+     **/
+
+    public void write(StringBuffer buffer, ScriptSession session)
+    {
+        buffer.append(_text);
+    }
+
+    public void addToken(IScriptToken token)
+    {
+        // Should never be invoked.
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/script/UniqueRule.java b/tapestry-framework/src/org/apache/tapestry/script/UniqueRule.java
new file mode 100644
index 0000000..6294f2e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/UniqueRule.java
@@ -0,0 +1,45 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.util.xml.RuleDirectedParser;
+import org.xml.sax.Attributes;
+
+/**
+ * Constructs a {@link org.apache.tapestry.script.UniqueToken}
+ * from an &lt;unique&gt; element, which contains full content.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * @since 3.0
+ */
+
+public class UniqueRule extends AbstractTokenRule
+{
+
+    public void endElement(RuleDirectedParser parser)
+    {
+        parser.pop();
+    }
+
+    public void startElement(RuleDirectedParser parser, Attributes attributes)
+    {
+        IScriptToken token = new UniqueToken(parser.getLocation());
+
+        addToParent(parser, token);
+        parser.push(token);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/script/UniqueToken.java b/tapestry-framework/src/org/apache/tapestry/script/UniqueToken.java
new file mode 100644
index 0000000..8441991
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/UniqueToken.java
@@ -0,0 +1,53 @@
+//  Copyright 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.tapestry.script;
+
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ * Writes out its child tokens only the first time it executes
+ * (with a given tag).  Uses
+ * {@link org.apache.tapestry.IRequestCycle#setAttribute(String, Object)}
+ * to identify whether a particular block has rendered yet.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * @since 3.0
+ */
+
+class UniqueToken extends AbstractToken
+{
+    public UniqueToken(ILocation location)
+    {
+        super(location);
+    }
+
+    public void write(StringBuffer buffer, ScriptSession session)
+    {
+        IRequestCycle cycle = session.getRequestCycle();
+
+        ILocation location = getLocation();
+        String tag = "<unique> " + location.toString();
+
+        if (cycle.getAttribute(tag) != null)
+            return;
+
+        cycle.setAttribute(tag, Boolean.TRUE);
+
+        writeChildren(buffer, session);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/script/package.html b/tapestry-framework/src/org/apache/tapestry/script/package.html
new file mode 100644
index 0000000..d27729c
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/script/package.html
@@ -0,0 +1,19 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+<p>Parser and related classes for dynamically generating JavaScript for
+inclusion in an HTML response.  This is used by a number of
+tapestry components, including 
+{@link org.apache.tapestry.html.Rollover}, as well
+as {@link org.apache.tapestry.html.Script} (used for
+including arbitrary user-written JavaScript).
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/ApplicationSpecification.java b/tapestry-framework/src/org/apache/tapestry/spec/ApplicationSpecification.java
new file mode 100644
index 0000000..ce9687b
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/ApplicationSpecification.java
@@ -0,0 +1,62 @@
+//  Copyright 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.tapestry.spec;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+/**
+ *  Defines the configuration for a Tapestry application.  An ApplicationSpecification
+ *  extends {@link LibrarySpecification} by adding new properties
+ *  name and engineClassName.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+public class ApplicationSpecification
+    extends LibrarySpecification
+    implements IApplicationSpecification
+{
+    private String _name;
+    private String _engineClassName;
+
+    public String getName()
+    {
+        return _name;
+    }
+
+    public void setEngineClassName(String value)
+    {
+        _engineClassName = value;
+    }
+
+    public String getEngineClassName()
+    {
+        return _engineClassName;
+    }
+
+    public void setName(String name)
+    {
+        _name = name;
+    }
+
+    protected void extendDescription(ToStringBuilder builder)
+    {
+        builder.append("name", _name);
+        builder.append("engineClassName", _engineClassName);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/AssetSpecification.java b/tapestry-framework/src/org/apache/tapestry/spec/AssetSpecification.java
new file mode 100644
index 0000000..a069c6f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/AssetSpecification.java
@@ -0,0 +1,60 @@
+//  Copyright 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.tapestry.spec;
+
+/**
+ *  Defines an internal, external or private asset.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+public class AssetSpecification extends LocatablePropertyHolder implements IAssetSpecification
+{
+    private AssetType type;
+    protected String path;
+
+    /**
+     *  Returns the base path for the asset.  This may be interpreted as a URL, relative URL
+     *  or the path to a resource, depending on the type of asset.
+     *
+     **/
+
+    public String getPath()
+    {
+        return path;
+    }
+
+    public AssetType getType()
+    {
+        return type;
+    }
+
+    /** @since 3.0 **/
+
+    public void setPath(String path)
+    {
+        this.path = path;
+    }
+
+    /** @since 3.0 **/
+
+    public void setType(AssetType type)
+    {
+        this.type = type;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/AssetType.java b/tapestry-framework/src/org/apache/tapestry/spec/AssetType.java
new file mode 100644
index 0000000..1dbd468
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/AssetType.java
@@ -0,0 +1,58 @@
+//  Copyright 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.tapestry.spec;
+
+import org.apache.commons.lang.enum.Enum;
+
+/**
+ *  Defines the types of assets.
+ *
+ *  @see IAssetSpecification
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public final class AssetType extends Enum
+{
+    /**
+     *  An external resource.
+     *
+     **/
+
+    public static final AssetType EXTERNAL = new AssetType("EXTERNAL");
+
+    /**
+     *  A resource visible to the {@link javax.servlet.ServletContext}.
+     *
+     **/
+
+    public static final AssetType CONTEXT = new AssetType("CONTEXT");
+
+    /**
+     *  An internal resource visible only on the classpath.  Typically,
+     *  a resource package in a WAR or JAR file alongside the classes.
+     *
+     **/
+
+    public static final AssetType PRIVATE = new AssetType("PRIVATE");
+
+    private AssetType(String name)
+    {
+        super(name);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/BaseLocatable.java b/tapestry-framework/src/org/apache/tapestry/spec/BaseLocatable.java
new file mode 100644
index 0000000..e44e6f0
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/BaseLocatable.java
@@ -0,0 +1,45 @@
+//  Copyright 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.tapestry.spec;
+
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.ILocationHolder;
+
+/**
+ *  Base class for classes which implement
+ *  {@link org.apache.tapestry.ILocationHolder}.
+ * 
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class BaseLocatable implements ILocationHolder
+{
+	private ILocation _location;
+	
+    public void setLocation(ILocation location)
+    {
+    	_location = location;
+    }
+
+    public ILocation getLocation()
+    {
+        return _location;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/BeanLifecycle.java b/tapestry-framework/src/org/apache/tapestry/spec/BeanLifecycle.java
new file mode 100644
index 0000000..8b617c2
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/BeanLifecycle.java
@@ -0,0 +1,70 @@
+//  Copyright 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.tapestry.spec;
+
+import org.apache.commons.lang.enum.Enum;
+
+
+/**
+ *  An {@link Enum} of the different possible lifecycles for a JavaBean.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.4
+ * 
+ **/
+
+public class BeanLifecycle extends Enum
+{
+	/**
+	 *  No lifecycle; the bean is created fresh on each reference and not retained.
+	 *
+	 **/
+
+	public static final BeanLifecycle NONE = new BeanLifecycle("NONE");
+
+	/**
+	 * The standard lifecycle; the bean is retained for the
+	 * duration of the request cycle and is discarded at the end of the
+	 * request cycle.
+	 *
+	 **/
+
+	public static final BeanLifecycle REQUEST = new BeanLifecycle("REQUEST");
+
+	/**
+	 * The bean is created once and reused for the lifespan of the page
+	 * containing the component.
+	 *
+	 **/
+
+	public static final BeanLifecycle PAGE = new BeanLifecycle("PAGE");
+
+    /**
+     *  The bean is create and reused until the end of the current render,
+     *  at which point it is discarded.
+     * 
+     *  @since 2.2
+     * 
+     **/
+    
+    public static final BeanLifecycle RENDER = new BeanLifecycle("RENDER");
+    
+	private BeanLifecycle(String name)
+	{
+		super(name);
+	}
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/BeanSpecification.java b/tapestry-framework/src/org/apache/tapestry/spec/BeanSpecification.java
new file mode 100644
index 0000000..6935e44
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/BeanSpecification.java
@@ -0,0 +1,127 @@
+//  Copyright 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.tapestry.spec;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.tapestry.bean.IBeanInitializer;
+
+/**
+ *  A specification of a helper bean for a component.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.4
+ * 
+ **/
+
+public class BeanSpecification extends LocatablePropertyHolder implements IBeanSpecification
+{
+    protected String className;
+    protected BeanLifecycle lifecycle;
+
+    /** @since 1.0.9 **/
+    private String description;
+
+    /**
+     *  A List of {@link IBeanInitializer}.
+     *
+     **/
+
+    protected List initializers;
+
+    public String getClassName()
+    {
+        return className;
+    }
+
+    public BeanLifecycle getLifecycle()
+    {
+        return lifecycle;
+    }
+
+    /**
+     *  @since 1.0.5
+     *
+     **/
+
+    public void addInitializer(IBeanInitializer initializer)
+    {
+        if (initializers == null)
+            initializers = new ArrayList();
+
+        initializers.add(initializer);
+    }
+
+    /**
+     *  Returns the {@link List} of {@link IBeanInitializer}s.  The caller
+     *  should not modify this value!.  May return null if there
+     *  are no initializers.
+     *
+     *  @since 1.0.5
+     *
+     **/
+
+    public List getInitializers()
+    {
+        return initializers;
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer("BeanSpecification[");
+
+        buffer.append(className);
+        buffer.append(", lifecycle ");
+        buffer.append(lifecycle.getName());
+
+        if (initializers != null && initializers.size() > 0)
+        {
+            buffer.append(", ");
+            buffer.append(initializers.size());
+            buffer.append(" initializers");
+        }
+
+        buffer.append(']');
+
+        return buffer.toString();
+    }
+
+    public String getDescription()
+    {
+        return description;
+    }
+
+    public void setDescription(String desc)
+    {
+        description = desc;
+    }
+
+    /** @since 3.0 **/
+
+    public void setClassName(String className)
+    {
+        this.className = className;
+    }
+    
+    /** @since 3.0 **/
+    
+    public void setLifecycle(BeanLifecycle lifecycle)
+    {
+        this.lifecycle = lifecycle;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/BindingSpecification.java b/tapestry-framework/src/org/apache/tapestry/spec/BindingSpecification.java
new file mode 100644
index 0000000..01c40e0
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/BindingSpecification.java
@@ -0,0 +1,51 @@
+//  Copyright 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.tapestry.spec;
+
+/**
+ *  Stores a binding specification, which identifies the static value
+ *  or OGNL expression for the binding.  The name of the binding (which
+ *  matches a bindable property of the contined component) is implicitly known.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class BindingSpecification extends BaseLocatable implements IBindingSpecification
+{
+    private BindingType _type;
+    private String _value;
+	
+    public BindingType getType()
+    {
+        return _type;
+    }
+
+    public String getValue()
+    {
+        return _value;
+    }
+
+    public void setType(BindingType type)
+    {
+        _type = type;
+    }
+
+    public void setValue(String value)
+    {
+        _value = value;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/BindingType.java b/tapestry-framework/src/org/apache/tapestry/spec/BindingType.java
new file mode 100644
index 0000000..190ae4b
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/BindingType.java
@@ -0,0 +1,94 @@
+//  Copyright 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.tapestry.spec;
+
+import org.apache.commons.lang.enum.Enum;
+
+/**
+ *  Defines the different types of bindings possible for a component.
+ *  These are used in the {@link IBindingSpecification} and ultimately
+ *  used to create an instance of {@link org.apache.tapestry.IBinding}.
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public final class BindingType extends Enum
+{
+    /**
+     *  Indicates a {@link org.apache.tapestry.binding.StaticBinding}.
+     *
+     **/
+
+    public static final BindingType STATIC = new BindingType("STATIC");
+
+    /**
+     *  Indicates a standard {@link org.apache.tapestry.binding.ExpressionBinding}.
+     *
+     **/
+
+    public static final BindingType DYNAMIC = new BindingType("DYNAMIC");
+
+    /**
+     *  Indicates that an existing binding (from the container) will be
+     *  re-used.
+     *
+     **/
+
+    public static final BindingType INHERITED = new BindingType("INHERITED");
+
+    /**
+     *  Indicates a {@link org.apache.tapestry.binding.FieldBinding}.
+     *
+     *  <p>
+     *  Field bindings are only available in the 1.3 DTD.  The 1.4 DTD
+     *  does not support them (since OGNL expressions can do the same thing).
+     * 
+     **/
+
+    public static final BindingType FIELD = new BindingType("FIELD");
+
+    /**
+     *  Indicates a {@link org.apache.tapestry.binding.ListenerBinding}, a
+     *  specialized kind of binding that encapsulates a component listener
+     *  as a script.  Uses a subclass of {@link BindingSpecification},
+     *  {@link ListenerBindingSpecification}.
+     *  {@link IListenerBindingSpecification}.
+     * 
+     *  @since 3.0
+     * 
+     **/
+    
+    public static final BindingType LISTENER = new BindingType("LISTENER");
+
+	/**
+	 *  A binding to one of a component's localized strings.
+	 * 
+	 *  @see org.apache.tapestry.IComponent#getString(String)
+	 * 
+	 *  @since 2.0.4
+	 * 
+	 **/
+	
+	public static final BindingType STRING = new BindingType("STRING");
+	
+    private BindingType(String name)
+    {
+        super(name);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/ComponentSpecification.java b/tapestry-framework/src/org/apache/tapestry/spec/ComponentSpecification.java
new file mode 100644
index 0000000..40ee214
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/ComponentSpecification.java
@@ -0,0 +1,603 @@
+//  Copyright 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.tapestry.spec;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  A specification for a component, as read from an XML specification file.
+ *
+ *  <p>A specification consists of
+ *  <ul>
+ *  <li>An implementing class
+ *  <li>An optional template
+ *  <li>An optional description
+ *  <li>A set of contained components
+ *  <li>Bindings for the properties of each contained component
+ *  <li>A set of named assets
+ *  <li>Definitions for helper beans
+ *  <li>Any reserved names (used for HTML attributes)
+ *  </ul>
+ *
+ *  <p>From this information, an actual component may be instantiated and
+ *  initialized.  Instantiating a component is usually a recursive process, since
+ *  to initialize a container component, it is necessary to instantiate and initialize
+ *  its contained components as well.
+ *
+ *  @see org.apache.tapestry.IComponent
+ *  @see IContainedComponent
+ *  @see org.apache.tapestry.engine.IPageLoader
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+public class ComponentSpecification extends LocatablePropertyHolder implements IComponentSpecification
+{
+    private String _componentClassName; 
+
+    /** @since 1.0.9 **/
+
+    private String _description;
+
+    /**
+     *  Keyed on component id, value is {@link IContainedComponent}.
+     *
+     **/
+
+    protected Map _components;
+
+    /**
+     *  Keyed on asset name, value is {@link IAssetSpecification}.
+     *
+     **/
+
+    protected Map _assets;
+
+    /**
+     *  Defines all formal parameters.  Keyed on parameter name, value is
+     * {@link IParameterSpecification}.
+     *
+     **/
+
+    protected Map _parameters;
+
+    /**
+     *  Defines all helper beans.  Keyed on name, value is {@link IBeanSpecification}.
+     *
+     *  @since 1.0.4
+     **/
+
+    protected Map _beans;
+
+    /**
+     *  The names of all reserved informal parameter names (as lower-case).  This
+     *  allows the page loader to filter out any informal parameters during page load,
+     *  rather than during render.
+     *
+     *   @since 1.0.5
+     *
+     **/
+
+    protected Set _reservedParameterNames;
+
+    /**
+     *  Is the component allowed to have a body (that is, wrap other elements?).
+     *
+     **/
+
+    private boolean _allowBody = true;
+
+    /**
+     *  Is the component allow to have informal parameter specified.
+     *
+     **/
+
+    private boolean _allowInformalParameters = true;
+
+    /**
+     *  The XML Public Id used when the page or component specification was read
+     *  (if applicable).
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    private String _publicId;
+
+    /**
+     *  Indicates that the specification is for a page, not a component.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    private boolean _pageSpecification;
+
+    /**
+     *  The location from which the specification was obtained.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    private IResourceLocation _specificationLocation;
+
+    /**
+     *  A Map of {@link IPropertySpecification} keyed on the name
+     *  of the property.
+     *
+     *  @since 3.0
+     * 
+     **/
+
+    private Map _propertySpecifications;
+
+    /**
+     * @throws IllegalArgumentException if the name already exists.
+     *
+     **/
+
+    public void addAsset(String name, IAssetSpecification asset)
+    {
+        if (_assets == null)
+            _assets = new HashMap();
+
+        if (_assets.containsKey(name))
+            throw new IllegalArgumentException(
+                Tapestry.format("ComponentSpecification.duplicate-asset", this, name));
+
+        _assets.put(name, asset);
+    }
+
+    /**
+     *  @throws IllegalArgumentException if the id is already defined.
+     *
+     **/
+
+    public void addComponent(String id, IContainedComponent component)
+    {
+        if (_components == null)
+            _components = new HashMap();
+
+        if (_components.containsKey(id))
+            throw new IllegalArgumentException(
+                Tapestry.format("ComponentSpecification.duplicate-component", this, id));
+
+        _components.put(id, component);
+    }
+
+    /**
+     *  Adds the parameter.   The name is added as a reserved name.
+     *
+     *  @throws IllegalArgumentException if the name already exists.
+     **/
+
+    public void addParameter(String name, IParameterSpecification spec)
+    {
+        if (_parameters == null)
+            _parameters = new HashMap();
+
+        if (_parameters.containsKey(name))
+            throw new IllegalArgumentException(
+                Tapestry.format("ComponentSpecification.duplicate-parameter", this, name));
+
+        _parameters.put(name, spec);
+
+        addReservedParameterName(name);
+    }
+
+    /**
+     *  Returns true if the component is allowed to wrap other elements (static HTML
+     *  or other components).  The default is true.
+     *
+     *  @see #setAllowBody(boolean)
+     *
+     **/
+
+    public boolean getAllowBody()
+    {
+        return _allowBody;
+    }
+
+    /**
+     *  Returns true if the component allows informal parameters (parameters
+     *  not formally defined).  Informal parameters are generally used to create
+     *  additional HTML attributes for an HTML tag rendered by the
+     *  component.  This is often used to specify JavaScript event handlers or the class
+     *  of the component (for Cascarding Style Sheets).
+     *
+     * <p>The default value is true.
+     *
+     *  @see #setAllowInformalParameters(boolean)
+     **/
+
+    public boolean getAllowInformalParameters()
+    {
+        return _allowInformalParameters;
+    }
+
+    /**
+     *  Returns the {@link IAssetSpecification} with the given name, or null
+     *  if no such specification exists.
+     *
+     *  @see #addAsset(String,IAssetSpecification)
+     **/
+
+    public IAssetSpecification getAsset(String name)
+    {
+
+        return (IAssetSpecification) get(_assets, name);
+    }
+
+    /**
+     *  Returns a <code>List</code>
+     *  of the String names of all assets, in alphabetical
+     *  order
+     *
+     **/
+
+    public List getAssetNames()
+    {
+        return sortedKeys(_assets);
+    }
+
+    /**
+     *  Returns the specification of a contained component with the given id, or
+     *  null if no such contained component exists.
+     *
+     *  @see #addComponent(String, IContainedComponent)
+     *
+     **/
+
+    public IContainedComponent getComponent(String id)
+    {
+        return (IContainedComponent) get(_components, id);
+    }
+
+    public String getComponentClassName()
+    {
+        return _componentClassName;
+    }
+
+    /**
+     *  Returns an <code>List</code>
+     *  of the String names of the {@link IContainedComponent}s
+     *  for this component.
+     *
+     *  @see #addComponent(String, IContainedComponent)
+     *
+     **/
+
+    public List getComponentIds()
+    {
+        return sortedKeys(_components);
+    }
+
+    /**
+     *  Returns the specification of a parameter with the given name, or
+     *  null if no such parameter exists.
+     *
+     *  @see #addParameter(String, IParameterSpecification)
+     *
+     **/
+
+    public IParameterSpecification getParameter(String name)
+    {
+        return (IParameterSpecification) get(_parameters, name);
+    }
+
+    /**
+     *  Returns a List of
+     *  of String names of all parameters.  This list
+     *  is in alphabetical order.
+     *
+     *  @see #addParameter(String, IParameterSpecification)
+     *
+     **/
+
+    public List getParameterNames()
+    {
+        return sortedKeys(_parameters);
+    }
+
+    public void setAllowBody(boolean value)
+    {
+        _allowBody = value;
+    }
+
+    public void setAllowInformalParameters(boolean value)
+    {
+        _allowInformalParameters = value;
+    }
+
+    public void setComponentClassName(String value)
+    {
+        _componentClassName = value;
+    }
+
+    /**
+     *  @since 1.0.4
+     *
+     *  @throws IllegalArgumentException if the bean already has a specification.
+     **/
+
+    public void addBeanSpecification(String name, IBeanSpecification specification)
+    {
+        if (_beans == null)
+            _beans = new HashMap();
+
+        else
+            if (_beans.containsKey(name))
+                throw new IllegalArgumentException(
+                    Tapestry.format("ComponentSpecification.duplicate-bean", this, name));
+
+        _beans.put(name, specification);
+    }
+
+    /**
+     * Returns the {@link IBeanSpecification} for the given name, or null
+     * if not such specification exists.
+     *
+     * @since 1.0.4
+     *
+     **/
+
+    public IBeanSpecification getBeanSpecification(String name)
+    {
+        if (_beans == null)
+            return null;
+
+        return (IBeanSpecification) _beans.get(name);
+    }
+
+    /**
+     *  Returns an unmodifiable collection of the names of all beans.
+     *
+     **/
+
+    public Collection getBeanNames()
+    {
+        if (_beans == null)
+            return Collections.EMPTY_LIST;
+
+        return Collections.unmodifiableCollection(_beans.keySet());
+    }
+
+    /**
+     *  Adds the value as a reserved name.  Reserved names are not allowed
+     *  as the names of informal parameters.  Since the comparison is
+     *  caseless, the value is converted to lowercase before being
+     *  stored.
+     *
+     *  @since 1.0.5
+     *
+     **/
+
+    public void addReservedParameterName(String value)
+    {
+        if (_reservedParameterNames == null)
+            _reservedParameterNames = new HashSet();
+
+        _reservedParameterNames.add(value.toLowerCase());
+    }
+
+    /**
+     *  Returns true if the value specified is in the reserved name list.
+     *  The comparison is caseless.  All formal parameters are automatically
+     *  in the reserved name list, as well as any additional
+     *  reserved names specified in the component specification.  The latter
+     *  refer to HTML attributes generated directly by the component.
+     *
+     *  @since 1.0.5
+     *
+     **/
+
+    public boolean isReservedParameterName(String value)
+    {
+        if (_reservedParameterNames == null)
+            return false;
+
+        return _reservedParameterNames.contains(value.toLowerCase());
+    }
+
+    public String toString()
+    {
+        ToStringBuilder builder = new ToStringBuilder(this);
+
+        builder.append("componentClassName", _componentClassName);
+        builder.append("pageSpecification", _pageSpecification);
+        builder.append("specificationLocation", _specificationLocation);
+        builder.append("allowBody", _allowBody);
+        builder.append("allowInformalParameter", _allowInformalParameters);
+
+        return builder.toString();
+    }
+
+    /**
+     *  Returns the documentation for this component.
+     * 
+     *  @since 1.0.9
+     **/
+
+    public String getDescription()
+    {
+        return _description;
+    }
+
+    /**
+     *  Sets the documentation for this component.
+     * 
+     *  @since 1.0.9
+     **/
+
+    public void setDescription(String description)
+    {
+        _description = description;
+    }
+
+    /**
+     *  Returns the XML Public Id for the specification file, or null
+     *  if not applicable.
+     * 
+     *  <p>
+     *  This method exists as a convienience for the Spindle plugin.
+     *  A previous method used an arbitrary version string, the
+     *  public id is more useful and less ambiguous.
+     *  
+     *  @since 2.2
+     * 
+     **/
+
+    public String getPublicId()
+    {
+        return _publicId;
+    }
+
+    /** @since 2.2 **/
+
+    public void setPublicId(String publicId)
+    {
+        _publicId = publicId;
+    }
+
+    /** 
+     * 
+     *  Returns true if the specification is known to be a page
+     *  specification and not a component specification.  Earlier versions
+     *  of the framework did not distinguish between the two, but starting
+     *  in 2.2, there are seperate XML entities for pages and components.
+     *  Pages omit several attributes and entities related
+     *  to parameters, as parameters only make sense for components.
+     *  
+     *  @since 2.2 
+     * 
+     **/
+
+    public boolean isPageSpecification()
+    {
+        return _pageSpecification;
+    }
+
+    /** @since 2.2 **/
+
+    public void setPageSpecification(boolean pageSpecification)
+    {
+        _pageSpecification = pageSpecification;
+    }
+
+    /** @since 2.2 **/
+
+    private List sortedKeys(Map input)
+    {
+        if (input == null)
+            return Collections.EMPTY_LIST;
+
+        List result = new ArrayList(input.keySet());
+
+        Collections.sort(result);
+
+        return result;
+    }
+
+    /** @since 2.2 **/
+
+    private Object get(Map map, Object key)
+    {
+        if (map == null)
+            return null;
+
+        return map.get(key);
+    }
+
+    /** @since 3.0 **/
+
+    public IResourceLocation getSpecificationLocation()
+    {
+        return _specificationLocation;
+    }
+
+    /** @since 3.0 **/
+
+    public void setSpecificationLocation(IResourceLocation specificationLocation)
+    {
+        _specificationLocation = specificationLocation;
+    }
+
+    /**
+     *  Adds a new property specification.  The name of the property must
+     *  not already be defined (and must not change after being added).
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    public void addPropertySpecification(IPropertySpecification spec)
+    {
+        if (_propertySpecifications == null)
+            _propertySpecifications = new HashMap();
+
+        String name = spec.getName();
+
+        if (_propertySpecifications.containsKey(name))
+            throw new IllegalArgumentException(
+                Tapestry.format(
+                    "ComponentSpecification.duplicate-property-specification",
+                    this,
+                    name));
+
+        _propertySpecifications.put(name, spec);
+    }
+
+    /**
+     *  Returns a sorted, immutable list of the names of all 
+     *  {@link org.apache.tapestry.spec.IPropertySpecification}s.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    public List getPropertySpecificationNames()
+    {
+        return sortedKeys(_propertySpecifications);
+    }
+
+    /**
+     *  Returns the named {@link org.apache.tapestry.spec.IPropertySpecification},
+     *  or null  if no such specification exist.
+     * 
+     *  @since 3.0
+     *  @see #addPropertySpecification(IPropertySpecification)
+     * 
+     **/
+
+    public IPropertySpecification getPropertySpecification(String name)
+    {
+        return (IPropertySpecification) get(_propertySpecifications, name);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/ContainedComponent.java b/tapestry-framework/src/org/apache/tapestry/spec/ContainedComponent.java
new file mode 100644
index 0000000..89d6713
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/ContainedComponent.java
@@ -0,0 +1,137 @@
+//  Copyright 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.tapestry.spec;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Defines a contained component.  This includes the information needed to
+ * get the contained component's specification, as well as any bindings
+ * for the component.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * 
+ **/
+
+public class ContainedComponent extends LocatablePropertyHolder implements IContainedComponent
+{
+	private String type;
+
+	private String copyOf;
+    
+    private boolean inheritInformalParameters;
+
+	protected Map bindings;
+
+	private static final int MAP_SIZE = 3;
+
+	/**
+	 *  Returns the named binding, or null if the binding does not
+	 *  exist.
+	 *
+	 **/
+
+	public IBindingSpecification getBinding(String name)
+	{
+		if (bindings == null)
+			return null;
+
+		return (IBindingSpecification) bindings.get(name);
+	}
+
+	/**
+	 *  Returns an umodifiable <code>Collection</code>
+	 *  of Strings, each the name of one binding
+	 *  for the component.
+	 *
+	 **/
+
+	public Collection getBindingNames()
+	{
+		if (bindings == null)
+			return Collections.EMPTY_LIST;
+
+		return Collections.unmodifiableCollection(bindings.keySet());
+	}
+
+	public String getType()
+	{
+		return type;
+	}
+
+	public void setBinding(String name, IBindingSpecification spec)
+	{
+		if (bindings == null)
+			bindings = new HashMap(MAP_SIZE);
+
+		bindings.put(name, spec);
+	}
+
+	public void setType(String value)
+	{
+		type = value;
+	}
+
+	/**
+	 * 	Sets the String Id of the component being copied from.
+	 *  For use by IDE tools like Spindle.
+	 * 
+	 *  @since 1.0.9
+	 **/
+
+	public void setCopyOf(String id)
+	{
+		copyOf = id;
+	}
+
+	/**
+	 * 	Returns the id of the component being copied from.
+	 *  For use by IDE tools like Spindle.
+	 * 
+	 *  @since 1.0.9
+	 **/
+
+	public String getCopyOf()
+	{
+		return copyOf;
+	}
+
+	/**
+     * Returns whether the contained component will inherit 
+     * the informal parameters of its parent. 
+     * 
+	 * @since 3.0
+	 **/
+	public boolean getInheritInformalParameters()
+	{
+		return inheritInformalParameters;
+	}
+
+	/**
+     * Sets whether the contained component will inherit 
+     * the informal parameters of its parent. 
+     * 
+     * @since 3.0
+	 */
+	public void setInheritInformalParameters(boolean value)
+	{
+		inheritInformalParameters = value;
+	}
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/Direction.java b/tapestry-framework/src/org/apache/tapestry/spec/Direction.java
new file mode 100644
index 0000000..aa92714
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/Direction.java
@@ -0,0 +1,121 @@
+//  Copyright 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.tapestry.spec;
+
+import org.apache.commons.lang.enum.Enum;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Represents different types of parameters.  Currently only 
+ *  in and custom are supported, but this will likely change
+ *  when Tapestry supports out parameters is some form (that reflects
+ *  form style processing).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.0.3
+ *
+ */
+ 
+public class Direction extends Enum
+{
+    /**
+     *  The parameter value is input only; the component property value
+     *  is unchanged or not relevant after the component renders.
+     *  The property is set from the binding before the component renders,
+     *  then reset to initial value after the component renders.
+     * 
+     */
+    
+	public static final Direction IN = new Direction("IN");
+	
+    
+    /**
+     *  Encapsulates the semantics of a form component's value parameter.
+     * 
+     *  <p>The parameter is associated with a {@link org.apache.tapestry.form.IFormComponent}.
+     *  The property value is set from the binding before the component renders (when renderring,
+     *  but not when rewinding).
+     *  The binding is updated from the property value
+     *  after after the component renders when the
+     *  <b>containing form</b> is <b>rewinding</b>, <i>and</i>
+     *  the component is not {@link org.apache.tapestry.form.IFormComponent#isDisabled() disabled}.
+     * 
+     *  @since 2.2
+     * 
+     */
+
+    public static final Direction FORM = new Direction("FORM", false);    
+    
+	/**
+	 *  Processing of the parameter is entirely the responsibility
+	 *  of the component, which must obtain an manipulate
+	 *  the {@link org.apache.tapestry.IBinding} (if any) for the parameter.
+	 * 
+	 **/
+	
+	public static final Direction CUSTOM = new Direction("CUSTOM");
+
+
+	/**
+	 *  Causes a synthetic property to be created that automatically
+	 *  references and de-references the underlying binding.
+	 * 
+	 *  @since 3.0
+	 * 
+	 */
+	
+	public static final Direction AUTO = new Direction("AUTO");
+	
+	/**
+	 * If true, then this direction is allowed with invariant bindings (the usual case).
+	 * If false, then {@link org.apache.tapestry.param.ParameterManager} will not allow
+	 * an invariant binding.
+	 * 
+	 * @since 3.0
+	 */
+	
+	private boolean _allowInvariant;
+	
+    protected Direction(String name)
+    {
+        this(name, true);
+    }
+
+	protected Direction(String name, boolean allowInvariant)
+	{
+		super(name);
+		
+		_allowInvariant = allowInvariant;
+	}
+	
+	/**
+	 * @since 3.0
+	 */
+	public boolean getAllowInvariant()
+	{
+		return _allowInvariant;
+	}
+
+    /**
+     *  Returns a user-presentable name for the direction.
+     * 
+     */
+    
+    public String getDisplayName()
+    {
+        return Tapestry.getMessage("Direction." + getName());
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/ExtensionSpecification.java b/tapestry-framework/src/org/apache/tapestry/spec/ExtensionSpecification.java
new file mode 100644
index 0000000..4fe8e7e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/ExtensionSpecification.java
@@ -0,0 +1,182 @@
+//  Copyright 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.tapestry.spec;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.util.prop.OgnlUtils;
+
+/**
+ *  Defines an "extension", which is much like a helper bean, but 
+ *  is part of a library or application specification (and has the same
+ *  lifecycle as the application).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.2
+ * 
+ **/
+
+public class ExtensionSpecification
+    extends LocatablePropertyHolder
+    implements IExtensionSpecification
+{
+    private static final Log LOG = LogFactory.getLog(ExtensionSpecification.class);
+
+    private String _className;
+    protected Map _configuration = new HashMap();
+    private boolean _immediate;
+
+    public String getClassName()
+    {
+        return _className;
+    }
+
+    public void setClassName(String className)
+    {
+        _className = className;
+    }
+
+    public void addConfiguration(String propertyName, Object value)
+    {
+        if (_configuration.containsKey(propertyName))
+            throw new IllegalArgumentException(
+                Tapestry.format(
+                    "ExtensionSpecification.duplicate-property",
+                    this,
+                    propertyName));
+
+        _configuration.put(propertyName, value);
+    }
+
+    /**
+     *  Returns an immutable Map of the configuration; keyed on property name,
+     *  with values as properties to assign.
+     * 
+     **/
+
+    public Map getConfiguration()
+    {
+        return Collections.unmodifiableMap(_configuration);
+    }
+
+    /**
+     *  Invoked to instantiate an instance of the extension and return it.
+     *  It also configures properties of the extension.
+     * 
+     **/
+
+    public Object instantiateExtension(IResourceResolver resolver)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Instantiating extension class " + _className + ".");
+        Class extensionClass = null;
+        Object result = null;
+
+        try
+        {
+            extensionClass = resolver.findClass(_className);
+        }
+        catch (Exception ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("ExtensionSpecification.bad-class", _className),
+                getLocation(),
+                ex);
+        }
+
+        result = instantiateInstance(extensionClass, result);
+
+        initializeProperties(resolver, result);
+
+        return result;
+    }
+
+    private void initializeProperties(IResourceResolver resolver, Object extension)
+    {
+        if (_configuration.isEmpty())
+            return;
+
+        Iterator i = _configuration.entrySet().iterator();
+        while (i.hasNext())
+        {
+            Map.Entry entry = (Map.Entry) i.next();
+
+            String propertyName = (String) entry.getKey();
+
+            OgnlUtils.set(propertyName, resolver, extension, entry.getValue());
+        }
+    }
+
+    private Object instantiateInstance(Class extensionClass, Object result)
+    {
+        try
+        {
+            result = extensionClass.newInstance();
+        }
+        catch (Exception ex)
+        {
+            throw new ApplicationRuntimeException(ex.getMessage(), getLocation(), ex);
+        }
+
+        return result;
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer("ExtensionSpecification@");
+        buffer.append(Integer.toHexString(hashCode()));
+        buffer.append('[');
+        buffer.append(_className);
+
+        if (_configuration != null)
+        {
+            buffer.append(' ');
+            buffer.append(_configuration);
+        }
+
+        buffer.append(']');
+
+        return buffer.toString();
+    }
+
+    /**
+     *  Returns true if the extensions should be instantiated
+     *  immediately after the containing 
+     *  {@link org.apache.tapestry.spec.LibrarySpecification}
+     *  if parsed.  Non-immediate extensions are instantiated
+     *  only as needed.
+     * 
+     **/
+
+    public boolean isImmediate()
+    {
+        return _immediate;
+    }
+
+    public void setImmediate(boolean immediate)
+    {
+        _immediate = immediate;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/IApplicationSpecification.java b/tapestry-framework/src/org/apache/tapestry/spec/IApplicationSpecification.java
new file mode 100644
index 0000000..96c679e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/IApplicationSpecification.java
@@ -0,0 +1,49 @@
+//  Copyright 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.tapestry.spec;
+
+
+
+/**
+ *  Defines and interface for the configuration for a Tapestry application.  An ApplicationSpecification
+ *  extends {@link ILibrarySpecification} by adding new properties 
+ *  name and engineClassName.
+ *
+ *  @author Geoffrey Longman
+ *  @version $Id$
+ *
+ **/
+
+public interface IApplicationSpecification extends ILibrarySpecification
+{
+    /**
+     *  Returns a "user friendly" name for the application (which is optional).
+     * 
+     **/
+    
+    public String getName();
+
+    public void setEngineClassName(String value);
+    
+    /**
+     *  Returns the name of the class (which implements {@link org.apache.tapestry.IEngine}).
+     *  May return null, in which case a default is used.
+     * 
+     **/
+    
+    public String getEngineClassName();
+    
+    public void setName(String name);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/IAssetSpecification.java b/tapestry-framework/src/org/apache/tapestry/spec/IAssetSpecification.java
new file mode 100644
index 0000000..05d44b8
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/IAssetSpecification.java
@@ -0,0 +1,40 @@
+//  Copyright 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.tapestry.spec;
+
+import org.apache.tapestry.ILocatable;
+import org.apache.tapestry.ILocationHolder;
+import org.apache.tapestry.util.IPropertyHolder;
+
+/**
+ *  Defines an internal, external or private asset.
+ * 
+ * @author glongman@intelligentworks.com
+ * @version $Id$
+ */
+public interface IAssetSpecification extends IPropertyHolder, ILocationHolder, ILocatable
+{
+    /**
+     *  Returns the base path for the asset.  This may be interpreted as a URL, relative URL
+     *  or the path to a resource, depending on the type of asset.
+     *
+     **/
+    public abstract String getPath();
+    public abstract AssetType getType();
+    /** @since 3.0 **/
+    public abstract void setPath(String path);
+    /** @since 3.0 **/
+    public abstract void setType(AssetType type);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/IBeanSpecification.java b/tapestry-framework/src/org/apache/tapestry/spec/IBeanSpecification.java
new file mode 100644
index 0000000..1127b6f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/IBeanSpecification.java
@@ -0,0 +1,55 @@
+//  Copyright 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.tapestry.spec;
+
+import java.util.List;
+
+import org.apache.tapestry.ILocatable;
+import org.apache.tapestry.ILocationHolder;
+import org.apache.tapestry.bean.IBeanInitializer;
+import org.apache.tapestry.util.IPropertyHolder;
+
+/**
+ *  A specification of a helper bean for a component.
+ *
+ * @author glongman@intelligentworks.com
+ * @version $Id$
+ */
+public interface IBeanSpecification extends IPropertyHolder, ILocationHolder, ILocatable
+{
+    public abstract String getClassName();
+    public abstract BeanLifecycle getLifecycle();
+    /**
+     *  @since 1.0.5
+     *
+     **/
+    public abstract void addInitializer(IBeanInitializer initializer);
+    /**
+     *  Returns the {@link List} of {@link IBeanInitializer}s.  The caller
+     *  should not modify this value!.  May return null if there
+     *  are no initializers.
+     *
+     *  @since 1.0.5
+     *
+     **/
+    public abstract List getInitializers();
+    public abstract String toString();
+    public abstract String getDescription();
+    public abstract void setDescription(String desc);
+    /** @since 3.0 **/
+    public abstract void setClassName(String className);
+    /** @since 3.0 **/
+    public abstract void setLifecycle(BeanLifecycle lifecycle);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/IBindingSpecification.java b/tapestry-framework/src/org/apache/tapestry/spec/IBindingSpecification.java
new file mode 100644
index 0000000..07e427a
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/IBindingSpecification.java
@@ -0,0 +1,34 @@
+//  Copyright 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.tapestry.spec;
+
+import org.apache.tapestry.ILocatable;
+import org.apache.tapestry.ILocationHolder;
+
+/**
+ *  Stores a binding specification, which identifies the static value
+ *  or OGNL expression for the binding.  The name of the binding (which
+ *  matches a bindable property of the contined component) is implicitly known.
+ *
+ * @author glongman@intelligentworks.com
+ * @version $Id$
+ */
+public interface IBindingSpecification extends ILocationHolder, ILocatable
+{
+    public abstract BindingType getType();
+    public abstract String getValue();
+    public abstract void setType(BindingType type);
+    public abstract void setValue(String value);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/IComponentSpecification.java b/tapestry-framework/src/org/apache/tapestry/spec/IComponentSpecification.java
new file mode 100644
index 0000000..28fdf07
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/IComponentSpecification.java
@@ -0,0 +1,254 @@
+//  Copyright 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.tapestry.spec;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.tapestry.ILocatable;
+import org.apache.tapestry.ILocationHolder;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.util.IPropertyHolder;
+
+/**
+ *  A specification for a component, as read from an XML specification file.
+ *
+ *  <p>A specification consists of
+ *  <ul>
+ *  <li>An implementing class
+ *  <li>An optional template
+ *  <li>An optional description
+ *  <li>A set of contained components
+ *  <li>Bindings for the properties of each contained component
+ *  <li>A set of named assets
+ *  <li>Definitions for helper beans
+ *  <li>Any reserved names (used for HTML attributes)
+ *  </ul>
+ *
+ *  <p>From this information, an actual component may be instantiated and
+ *  initialized.  Instantiating a component is usually a recursive process, since
+ *  to initialize a container component, it is necessary to instantiate and initialize
+ *  its contained components as well.
+ *
+ *  @see org.apache.tapestry.IComponent
+ *  @see IContainedComponent
+ *  @see IComponentSpecification
+ *  @see org.apache.tapestry.engine.IPageLoader
+ * 
+ * @author glongman@intelligentworks.com
+ * @version $Id$
+ */
+public interface IComponentSpecification extends IPropertyHolder, ILocationHolder, ILocatable
+{
+    /**
+     * @throws IllegalArgumentException if the name already exists.
+     *
+     **/
+    public abstract void addAsset(String name, IAssetSpecification asset);
+    /**
+     *  @throws IllegalArgumentException if the id is already defined.
+     *
+     **/
+    public abstract void addComponent(String id, IContainedComponent component);
+    /**
+     *  Adds the parameter.   The name is added as a reserved name.
+     *
+     *  @throws IllegalArgumentException if the name already exists.
+     **/
+    public abstract void addParameter(String name, IParameterSpecification spec);
+    /**
+     *  Returns true if the component is allowed to wrap other elements (static HTML
+     *  or other components).  The default is true.
+     *
+     *  @see #setAllowBody(boolean)
+     *
+     **/
+    public abstract boolean getAllowBody();
+    /**
+     *  Returns true if the component allows informal parameters (parameters
+     *  not formally defined).  Informal parameters are generally used to create
+     *  additional HTML attributes for an HTML tag rendered by the
+     *  component.  This is often used to specify JavaScript event handlers or the class
+     *  of the component (for Cascarding Style Sheets).
+     *
+     * <p>The default value is true.
+     *
+     *  @see #setAllowInformalParameters(boolean)
+     **/
+    public abstract boolean getAllowInformalParameters();
+    /**
+     *  Returns the {@link IAssetSpecification} with the given name, or null
+     *  if no such specification exists.
+     *
+     *  @see #addAsset(String,IAssetSpecification)
+     **/
+    public abstract IAssetSpecification getAsset(String name);
+    /**
+     *  Returns a <code>List</code>
+     *  of the String names of all assets, in alphabetical
+     *  order
+     *
+     **/
+    public abstract List getAssetNames();
+    /**
+     *  Returns the specification of a contained component with the given id, or
+     *  null if no such contained component exists.
+     *
+     *  @see #addComponent(String, IContainedComponent)
+     *
+     **/
+    public abstract IContainedComponent getComponent(String id);
+    public abstract String getComponentClassName();
+    /**
+     *  Returns an <code>List</code>
+     *  of the String names of the {@link IContainedComponent}s
+     *  for this component.
+     *
+     *  @see #addComponent(String, IContainedComponent)
+     *
+     **/
+    public abstract List getComponentIds();
+    /**
+     *  Returns the specification of a parameter with the given name, or
+     *  null if no such parameter exists.
+     *
+     *  @see #addParameter(String, IParameterSpecification)
+     *
+     **/
+    public abstract IParameterSpecification getParameter(String name);
+    /**
+     *  Returns a List of
+     *  of String names of all parameters.  This list
+     *  is in alphabetical order.
+     *
+     *  @see #addParameter(String, IParameterSpecification)
+     *
+     **/
+    public abstract List getParameterNames();
+    public abstract void setAllowBody(boolean value);
+    public abstract void setAllowInformalParameters(boolean value);
+    public abstract void setComponentClassName(String value);
+    /**
+     *  @since 1.0.4
+     *
+     *  @throws IllegalArgumentException if the bean already has a specification.
+     **/
+    public abstract void addBeanSpecification(String name, IBeanSpecification specification);
+    /**
+     * Returns the {@link IBeanSpecification} for the given name, or null
+     * if not such specification exists.
+     *
+     * @since 1.0.4
+     *
+     **/
+    public abstract IBeanSpecification getBeanSpecification(String name);
+    /**
+     *  Returns an unmodifiable collection of the names of all beans.
+     *
+     **/
+    public abstract Collection getBeanNames();
+    /**
+     *  Adds the value as a reserved name.  Reserved names are not allowed
+     *  as the names of informal parameters.  Since the comparison is
+     *  caseless, the value is converted to lowercase before being
+     *  stored.
+     *
+     *  @since 1.0.5
+     *
+     **/
+    public abstract void addReservedParameterName(String value);
+    /**
+     *  Returns true if the value specified is in the reserved name list.
+     *  The comparison is caseless.  All formal parameters are automatically
+     *  in the reserved name list, as well as any additional
+     *  reserved names specified in the component specification.  The latter
+     *  refer to HTML attributes generated directly by the component.
+     *
+     *  @since 1.0.5
+     *
+     **/
+    public abstract boolean isReservedParameterName(String value);
+    /**
+     *  Returns the documentation for this component.
+     * 
+     *  @since 1.0.9
+     **/
+    public abstract String getDescription();
+    /**
+     *  Sets the documentation for this component.
+     * 
+     *  @since 1.0.9
+     **/
+    public abstract void setDescription(String description);
+    /**
+     *  Returns the XML Public Id for the specification file, or null
+     *  if not applicable.
+     * 
+     *  <p>
+     *  This method exists as a convienience for the Spindle plugin.
+     *  A previous method used an arbitrary version string, the
+     *  public id is more useful and less ambiguous.
+     *  
+     *  @since 2.2
+     * 
+     **/
+    public abstract String getPublicId();
+    /** @since 2.2 **/
+    public abstract void setPublicId(String publicId);
+    /** 
+     * 
+     *  Returns true if the specification is known to be a page
+     *  specification and not a component specification.  Earlier versions
+     *  of the framework did not distinguish between the two, but starting
+     *  in 2.2, there are seperate XML entities for pages and components.
+     *  Pages omit several attributes and entities related
+     *  to parameters, as parameters only make sense for components.
+     *  
+     *  @since 2.2 
+     * 
+     **/
+    public abstract boolean isPageSpecification();
+    /** @since 2.2 **/
+    public abstract void setPageSpecification(boolean pageSpecification);
+    /** @since 3.0 **/
+    public abstract IResourceLocation getSpecificationLocation();
+    /** @since 3.0 **/
+    public abstract void setSpecificationLocation(IResourceLocation specificationLocation);
+    /**
+     *  Adds a new property specification.  The name of the property must
+     *  not already be defined (and must not change after being added).
+     * 
+     *  @since 3.0
+     * 
+     **/
+    public abstract void addPropertySpecification(IPropertySpecification spec);
+    /**
+     *  Returns a sorted, immutable list of the names of all 
+     *  {@link org.apache.tapestry.spec.IPropertySpecification}s.
+     * 
+     *  @since 3.0
+     * 
+     **/
+    public abstract List getPropertySpecificationNames();
+    /**
+     *  Returns the named {@link org.apache.tapestry.spec.IPropertySpecification},
+     *  or null  if no such specification exist.
+     * 
+     *  @since 3.0
+     *  @see #addPropertySpecification(IPropertySpecification)
+     * 
+     **/
+    public abstract IPropertySpecification getPropertySpecification(String name);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/IContainedComponent.java b/tapestry-framework/src/org/apache/tapestry/spec/IContainedComponent.java
new file mode 100644
index 0000000..8a30f81
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/IContainedComponent.java
@@ -0,0 +1,79 @@
+//  Copyright 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.tapestry.spec;
+
+import java.util.Collection;
+
+import org.apache.tapestry.ILocatable;
+import org.apache.tapestry.ILocationHolder;
+import org.apache.tapestry.util.IPropertyHolder;
+
+/**
+ * Defines a contained component.  This includes the information needed to
+ * get the contained component's specification, as well as any bindings
+ * for the component.
+
+ * @author glongman@intelligentworks.com
+ * @version $Id$
+ */
+public interface IContainedComponent extends IPropertyHolder, ILocationHolder, ILocatable
+{
+    /**
+     *  Returns the named binding, or null if the binding does not
+     *  exist.
+     *
+     **/
+    public abstract IBindingSpecification getBinding(String name);
+    /**
+     *  Returns an umodifiable <code>Collection</code>
+     *  of Strings, each the name of one binding
+     *  for the component.
+     *
+     **/
+    public abstract Collection getBindingNames();
+    public abstract String getType();
+    public abstract void setBinding(String name, IBindingSpecification spec);
+    public abstract void setType(String value);
+    /**
+     * 	Sets the String Id of the component being copied from.
+     *  For use by IDE tools like Spindle.
+     * 
+     *  @since 1.0.9
+     **/
+    public abstract void setCopyOf(String id);
+    /**
+     * 	Returns the id of the component being copied from.
+     *  For use by IDE tools like Spindle.
+     * 
+     *  @since 1.0.9
+     **/
+    public abstract String getCopyOf();
+
+    /**
+     * Returns whether the contained component will inherit 
+     * the informal parameters of its parent. 
+     * 
+     * @since 3.0
+     **/
+    public abstract boolean getInheritInformalParameters();
+
+    /**
+     * Sets whether the contained component will inherit 
+     * the informal parameters of its parent. 
+     * 
+     * @since 3.0
+     */
+    public abstract void setInheritInformalParameters(boolean value);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/IExtensionSpecification.java b/tapestry-framework/src/org/apache/tapestry/spec/IExtensionSpecification.java
new file mode 100644
index 0000000..5326f39
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/IExtensionSpecification.java
@@ -0,0 +1,59 @@
+//  Copyright 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.tapestry.spec;
+
+import java.util.Map;
+
+import org.apache.tapestry.ILocatable;
+import org.apache.tapestry.ILocationHolder;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.util.IPropertyHolder;
+
+/**
+ *  Defines an "extension", which is much like a helper bean, but 
+ *  is part of a library or application specification (and has the same
+ *  lifecycle as the application).
+ * 
+ * @author glongman@intelligentworks.com
+ * @version $Id$
+ */
+public interface IExtensionSpecification extends IPropertyHolder, ILocationHolder, ILocatable
+{
+    public abstract String getClassName();
+    public abstract void setClassName(String className);
+    public abstract void addConfiguration(String propertyName, Object value);
+    /**
+     *  Returns an immutable Map of the configuration; keyed on property name,
+     *  with values as properties to assign.
+     * 
+     **/
+    public abstract Map getConfiguration();
+    /**
+     *  Invoked to instantiate an instance of the extension and return it.
+     *  It also configures properties of the extension.
+     * 
+     **/
+    public abstract Object instantiateExtension(IResourceResolver resolver);
+    /**
+     *  Returns true if the extensions should be instantiated
+     *  immediately after the containing 
+     *  {@link org.apache.tapestry.spec.LibrarySpecification}
+     *  if parsed.  Non-immediate extensions are instantiated
+     *  only as needed.
+     * 
+     **/
+    public abstract boolean isImmediate();
+    public abstract void setImmediate(boolean immediate);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/ILibrarySpecification.java b/tapestry-framework/src/org/apache/tapestry/spec/ILibrarySpecification.java
new file mode 100644
index 0000000..f92a3d1
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/ILibrarySpecification.java
@@ -0,0 +1,215 @@
+//  Copyright 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.tapestry.spec;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.tapestry.ILocationHolder;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.util.IPropertyHolder;
+
+/**
+ *  Interface for the Specification for a library.  {@link org.apache.tapestry.spec.ApplicationSpecification}
+ *  is a specialized kind of library.
+ *
+ *  @author Geoffrey Longman
+ *  @version $Id$
+ *  @since 2.2
+ *
+ **/
+
+public interface ILibrarySpecification extends IPropertyHolder, ILocationHolder
+{
+
+    /**
+     *  Returns the specification path (within the classpath) for
+     *  an embedded library, or null if
+     *  no such library has been defined.
+     * 
+     **/
+    
+    public String getLibrarySpecificationPath(String id);
+
+    /**
+     *  Sets the specification path for an embedded library.
+     * 
+     *  @throws IllegalArgumentException if a library with the given
+     *  id already exists
+     * 
+     **/
+
+    public void setLibrarySpecificationPath(String id, String path);
+
+    /**
+     *  Returns a sorted list of library ids (or the empty list, but not null).
+     * 
+     **/
+    
+    public List getLibraryIds();
+    
+    public String getPageSpecificationPath(String name);
+
+    public void setPageSpecificationPath(String name, String path);
+
+    /**
+     *  Returns a sorted list of page names explicitly defined by this library,
+     *  or an empty list (but not null).
+     * 
+     **/
+    
+    public List getPageNames();
+    
+    public void setComponentSpecificationPath(String type, String path);
+
+    public String getComponentSpecificationPath(String type);
+
+    /**
+     *  Returns the simple types of all components defined in
+     *  this library.  Returns a list of strings in sorted order,
+     *  or an empty list (but not null).
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    public List getComponentTypes();
+    
+    public String getServiceClassName(String name);
+
+    /**
+     *  Returns a sorted list of service names (or an empty list, but
+     *  not null).
+     * 
+     **/
+    
+    public List getServiceNames();
+
+    public void setServiceClassName(String name, String className);
+
+
+    /**
+     * 
+     *  Returns the documentation for this library..
+     * 
+     * 
+     **/
+
+    public String getDescription();
+
+    /**
+     *  
+     *  Sets the documentation for this library.
+     * 
+     * 
+     **/
+
+    public void setDescription(String description);
+    
+    /**
+     *  Returns a Map of extensions; key is extension name, value is
+     *  {@link org.apache.tapestry.spec.IExtensionSpecification}.
+     *  May return null.  The returned Map is immutable.
+     * 
+     **/
+
+    public Map getExtensionSpecifications();
+
+    /**
+     *  Adds another extension specification.
+     *  
+     **/
+
+    public void addExtensionSpecification(String name, IExtensionSpecification extension);
+    
+    /**
+     *  Returns a sorted List of the names of all extensions.  May return the empty list,
+     *  but won't return null.
+     * 
+     **/
+
+    public List getExtensionNames();
+    
+    /**
+     *  Returns the named IExtensionSpecification, or null if it doesn't exist.
+     * 
+     **/
+
+    public IExtensionSpecification getExtensionSpecification(String name);
+    
+
+    /**
+     *  Returns an instantiated extension.  Extensions are created as needed and
+     *  cached for later use.
+     * 
+     *  @throws IllegalArgumentException if no extension specification exists for the
+     *  given name.
+     * 
+     **/
+
+    public Object getExtension(String name);
+
+    /**
+     *  Returns an instantiated extension, performing a check to ensure
+     *  that the extension is a subtype of the given class (or extends the given
+     *  interface).
+     * 
+     *  @throws IllegalArgumentException if no extension specification exists for
+     *  the given name, or if the extension fails the type check.
+     * 
+     *  @since 3.0
+     * 
+     **/
+    
+    public Object getExtension(String name, Class typeConstraint);
+
+    /**
+     *  Returns true if the named extension exists (or can be instantiated),
+     *  returns false if the named extension has no specification.
+     * 
+     **/
+    
+    public boolean checkExtension(String name);
+
+    /**
+     *  Invoked after the entire specification has been constructed
+     *  to instantiate any extensions marked immediate.
+     * 
+     **/
+
+    public void instantiateImmediateExtensions();
+
+    public IResourceResolver getResourceResolver();
+
+    public void setResourceResolver(IResourceResolver resolver);
+    
+    public String getPublicId();
+    
+    public void setPublicId(String value);
+
+    /**
+     *  Returns the location from which the specification was read.
+     * 
+     *  @since 3.0
+     * 
+     **/
+    
+    public IResourceLocation getSpecificationLocation();
+    
+    /** @since 3.0 **/
+    
+    public void setSpecificationLocation(IResourceLocation specificationLocation);    
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/IListenerBindingSpecification.java b/tapestry-framework/src/org/apache/tapestry/spec/IListenerBindingSpecification.java
new file mode 100644
index 0000000..ecd70f4
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/IListenerBindingSpecification.java
@@ -0,0 +1,35 @@
+//  Copyright 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.tapestry.spec;
+
+/**
+ *  Special interface of {@link org.apache.tapestry.spec.IBindingSpecification} used
+ *  to encapsulate additional information the additional information 
+ *  specific to listener bindings.  In an IListenerBindingSpecification, the
+ *  value property is the actual script (and is aliased as property script), 
+ *  but an additional property,
+ *  language, (which may be null) is needed.  This is the language
+ *  the script is written in. * 
+ * 
+ * @author glongman@intelligentworks.com
+ * @version $Id$
+ * @since 3.0
+ */
+public interface IListenerBindingSpecification extends IBindingSpecification
+{
+    public abstract String getLanguage();
+    public abstract String getScript();
+    public abstract void setLanguage(String language);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/IParameterSpecification.java b/tapestry-framework/src/org/apache/tapestry/spec/IParameterSpecification.java
new file mode 100644
index 0000000..6f7723b
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/IParameterSpecification.java
@@ -0,0 +1,93 @@
+//  Copyright 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.tapestry.spec;
+
+import org.apache.tapestry.ILocationHolder;
+
+/**
+ *  Defines a formal parameter to a component.  An <code>IParameterSpecification</code>
+ *  is contained by a {@link IComponentSpecification}.
+ *
+ *  <p>TBD: Identify arrays in some way.
+ * 
+ * @author glongman@intelligentworks.com
+ * @version $Id$
+ */
+public interface IParameterSpecification extends ILocationHolder
+{
+    /**
+     *  Returns the class name of the expected type of the parameter.  The default value
+     *  is <code>java.lang.Object</code> which matches anything.
+     *
+     **/
+    public abstract String getType();
+    /**
+     *  Returns true if the parameter is required by the component.
+     *  The default is false, meaning the parameter is optional.
+     *
+     **/
+    public abstract boolean isRequired();
+    public abstract void setRequired(boolean value);
+    /**
+     *  Sets the type of value expected for the parameter.  This can be
+     *  left blank to indicate any type.
+     * 
+     **/
+    public abstract void setType(String value);
+    /**
+     *  Returns the documentation for this parameter.
+     * 
+     *  @since 1.0.9
+     * 
+     **/
+    public abstract String getDescription();
+    /**
+     *  Sets the documentation for this parameter.
+     * 
+     *  @since 1.0.9
+     *    	 
+     **/
+    public abstract void setDescription(String description);
+    /**
+     *  Sets the property name (of the component class)
+     *  to connect the parameter to.
+     * 
+     **/
+    public abstract void setPropertyName(String propertyName);
+    /**
+     *  Returns the name of the JavaBeans property to connect the
+     *  parameter to.
+     * 
+     **/
+    public abstract String getPropertyName();
+    /**
+     *  Returns the parameter value direction, defaulting to {@link Direction#CUSTOM}
+     *  if not otherwise specified.
+     * 
+     **/
+    public abstract Direction getDirection();
+    public abstract void setDirection(Direction direction);
+    /**
+     *  Returns the default value of the JavaBeans property if no binding is provided
+     *  or null if it has not been specified
+     **/
+    public abstract String getDefaultValue();
+    /**
+     *  Sets the default value of the JavaBeans property if no binding is provided
+     * 
+     **/
+    public abstract void setDefaultValue(String defaultValue);
+    
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/IPropertySpecification.java b/tapestry-framework/src/org/apache/tapestry/spec/IPropertySpecification.java
new file mode 100644
index 0000000..cec6eaa
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/IPropertySpecification.java
@@ -0,0 +1,43 @@
+//  Copyright 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.tapestry.spec;
+
+import org.apache.tapestry.ILocationHolder;
+
+/**
+ *  Defines a transient or persistant property of a component or page.  
+ *  A {@link org.apache.tapestry.engine.IComponentClassEnhancer} uses this information
+ *  to create a subclass with the necessary instance variables and methods.  
+ *  
+ * @author glongman@intelligentworks.com
+ * @version $Id$
+ */
+public interface IPropertySpecification extends ILocationHolder
+{
+    public abstract String getInitialValue();
+    public abstract String getName();
+    public abstract boolean isPersistent();
+    public abstract String getType();
+    public abstract void setInitialValue(String initialValue);
+    /**
+     *  Sets the name of the property.  This should not be changed
+     *  once this IPropertySpecification is added to
+     *  a {@link org.apache.tapestry.spec.IComponentSpecification}.
+     * 
+     **/
+    public abstract void setName(String name);
+    public abstract void setPersistent(boolean persistant);
+    public abstract void setType(String type);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/LibrarySpecification.java b/tapestry-framework/src/org/apache/tapestry/spec/LibrarySpecification.java
new file mode 100644
index 0000000..0e60a4a
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/LibrarySpecification.java
@@ -0,0 +1,637 @@
+//  Copyright 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.tapestry.spec;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Specification for a library.  {@link org.apache.tapestry.spec.ApplicationSpecification}
+ *  is a specialized kind of library.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.2
+ *
+ **/
+
+public class LibrarySpecification extends LocatablePropertyHolder implements ILibrarySpecification
+{
+    /**
+     *  Resource resolver (used to instantiate extensions).
+     * 
+     **/
+
+    private IResourceResolver _resolver;
+
+    /**
+     *  Map of page name to page specification path.
+     * 
+     **/
+
+    private Map _pages;
+
+    /**
+     *  Map of component alias to component specification path.
+     * 
+     **/
+    private Map _components;
+
+    /**
+     *  Map of service name to service class name.
+     * 
+     **/
+
+    private Map _services;
+
+    /**
+     *  Map of library id to library specification path.
+     * 
+     **/
+
+    private Map _libraries;
+
+    private String _description;
+
+    /**
+     *  Map of extension name to {@link IExtensionSpecification}.
+     * 
+     **/
+
+    private Map _extensions;
+
+    /**
+     *  Map of extension name to Object for instantiated extensions.
+     * 
+     **/
+
+    private Map _instantiatedExtensions;
+
+    /**
+     *  The XML Public Id used when the library specification was read
+     *  (if applicable).
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    private String _publicId;
+
+    /**
+     *  The location of the specification.
+     * 
+     **/
+
+    private IResourceLocation _specificationLocation;
+
+    public String getLibrarySpecificationPath(String id)
+    {
+        return (String) get(_libraries, id);
+    }
+
+    /**
+     *  Sets the specification path for an embedded library.
+     * 
+     *  @throws IllegalArgumentException if a library with the given
+     *  id already exists
+     * 
+     **/
+
+    public void setLibrarySpecificationPath(String id, String path)
+    {
+        if (_libraries == null)
+            _libraries = new HashMap();
+
+        if (_libraries.containsKey(id))
+            throw new IllegalArgumentException(
+                Tapestry.format("LibrarySpecification.duplicate-child-namespace-id", id));
+
+        _libraries.put(id, path);
+    }
+
+    public List getLibraryIds()
+    {
+        return sortedKeys(_libraries);
+    }
+
+    public String getPageSpecificationPath(String name)
+    {
+        return (String) get(_pages, name);
+    }
+
+    public void setPageSpecificationPath(String name, String path)
+    {
+        if (_pages == null)
+            _pages = new HashMap();
+
+        if (_pages.containsKey(name))
+            throw new IllegalArgumentException(
+                Tapestry.format("LibrarySpecification.duplicate-page-name", name));
+
+        _pages.put(name, path);
+    }
+
+    public List getPageNames()
+    {
+        return sortedKeys(_pages);
+    }
+
+    public void setComponentSpecificationPath(String alias, String path)
+    {
+        if (_components == null)
+            _components = new HashMap();
+
+        if (_components.containsKey(alias))
+            throw new IllegalArgumentException(
+                Tapestry.format("LibrarySpecification.duplicate-component-alias", alias));
+
+        _components.put(alias, path);
+    }
+
+    public String getComponentSpecificationPath(String alias)
+    {
+        return (String) get(_components, alias);
+    }
+
+    /**
+     *  @since 3.0
+     * 
+     **/
+
+    public List getComponentTypes()
+    {
+        return sortedKeys(_components);
+    }
+
+    public String getServiceClassName(String name)
+    {
+        return (String) get(_services, name);
+    }
+
+    public List getServiceNames()
+    {
+        return sortedKeys(_services);
+    }
+
+    public void setServiceClassName(String name, String className)
+    {
+        if (_services == null)
+            _services = new HashMap();
+
+        if (_services.containsKey(name))
+            throw new IllegalArgumentException(
+                Tapestry.format("LibrarySpecification.duplicate-service-name", name));
+
+        _services.put(name, className);
+    }
+
+    private List sortedKeys(Map map)
+    {
+        if (map == null)
+            return Collections.EMPTY_LIST;
+
+        List result = new ArrayList(map.keySet());
+
+        Collections.sort(result);
+
+        return result;
+    }
+
+    private Object get(Map map, Object key)
+    {
+        if (map == null)
+            return null;
+
+        return map.get(key);
+    }
+
+    /**
+     * 
+     *  Returns the documentation for this library..
+     * 
+     * 
+     **/
+
+    public String getDescription()
+    {
+        return _description;
+    }
+
+    /**
+     *  
+     *  Sets the documentation for this library.
+     * 
+     * 
+     **/
+
+    public void setDescription(String description)
+    {
+        _description = description;
+    }
+
+    /**
+     *  Returns a Map of extensions; key is extension name, value is
+     *  {@link org.apache.tapestry.spec.IExtensionSpecification}.
+     *  May return null.  The returned Map is immutable.
+     * 
+     **/
+
+    public Map getExtensionSpecifications()
+    {
+        if (_extensions == null)
+            return null;
+
+        return Collections.unmodifiableMap(_extensions);
+    }
+
+    /**
+     *  Adds another extension specification.
+     * 
+     *  @throws IllegalArgumentException if an extension with the given name already exists.
+     * 
+     **/
+
+    public void addExtensionSpecification(String name, IExtensionSpecification extension)
+    {
+        if (_extensions == null)
+            _extensions = new HashMap();
+
+        if (_extensions.containsKey(name))
+            throw new IllegalArgumentException(
+                Tapestry.format("LibrarySpecification.duplicate-extension-name", this, name));
+
+        _extensions.put(name, extension);
+    }
+
+    /**
+     *  Returns a sorted List of the names of all extensions.  May return the empty list,
+     *  but won't return null.
+     * 
+     **/
+
+    public synchronized List getExtensionNames()
+    {
+        return sortedKeys(_instantiatedExtensions);
+    }
+
+    /**
+     *  Returns the named IExtensionSpecification, or null if it doesn't exist.
+     * 
+     **/
+
+    public IExtensionSpecification getExtensionSpecification(String name)
+    {
+        if (_extensions == null)
+            return null;
+
+        return (IExtensionSpecification) _extensions.get(name);
+    }
+
+    /**
+     *  Returns true if this library specification has a specification
+     *  for the named extension.
+     * 
+     **/
+
+    public boolean checkExtension(String name)
+    {
+        if (_extensions == null)
+            return false;
+
+        return _extensions.containsKey(name);
+    }
+
+    /**
+     *  Returns an instantiated extension.  Extensions are created as needed and
+     *  cached for later use.
+     * 
+     *  @throws IllegalArgumentException if no extension specification exists for the
+     *  given name.
+     * 
+     **/
+
+    public synchronized Object getExtension(String name)
+    {
+        return getExtension(name, null);
+    }
+
+    /** @since 3.0 **/
+
+    public synchronized Object getExtension(String name, Class typeConstraint)
+    {
+        if (_instantiatedExtensions == null)
+            _instantiatedExtensions = new HashMap();
+
+        Object result = _instantiatedExtensions.get(name);
+        IExtensionSpecification spec = getExtensionSpecification(name);
+
+        if (spec == null)
+            throw new IllegalArgumentException(
+                Tapestry.format("LibrarySpecification.no-such-extension", name));
+
+        if (result == null)
+        {
+
+            result = spec.instantiateExtension(_resolver);
+
+            _instantiatedExtensions.put(name, result);
+        }
+
+        if (typeConstraint != null)
+            applyTypeConstraint(name, result, typeConstraint, spec.getLocation());
+
+        return result;
+    }
+
+    /**
+     *  Checks that an extension conforms to the supplied type constraint.
+     * 
+     *  @throws IllegalArgumentException if the extension fails the check.
+     * 
+     *  @since 3.0
+     *  
+     **/
+
+    protected void applyTypeConstraint(
+        String name,
+        Object extension,
+        Class typeConstraint,
+        ILocation location)
+    {
+        Class extensionClass = extension.getClass();
+
+        // Can you assign an instance of the extension to a variable
+        // of type typeContraint legally?
+
+        if (typeConstraint.isAssignableFrom(extensionClass))
+            return;
+
+        String key =
+            typeConstraint.isInterface()
+                ? "LibrarySpecification.extension-does-not-implement-interface"
+                : "LibrarySpecification.extension-not-a-subclass";
+
+        throw new ApplicationRuntimeException(
+            Tapestry.format(key, name, extensionClass.getName(), typeConstraint.getName()),
+            location,
+            null);
+    }
+
+    /**
+     *  Invoked after the entire specification has been constructed
+     *  to instantiate any extensions marked immediate.
+     * 
+     **/
+
+    public synchronized void instantiateImmediateExtensions()
+    {
+        if (_extensions == null)
+            return;
+
+        Iterator i = _extensions.entrySet().iterator();
+
+        while (i.hasNext())
+        {
+            Map.Entry entry = (Map.Entry) i.next();
+
+            IExtensionSpecification spec = (IExtensionSpecification) entry.getValue();
+
+            if (!spec.isImmediate())
+                continue;
+
+            String name = (String) entry.getKey();
+
+            getExtension(name);
+        }
+
+    }
+
+    public IResourceResolver getResourceResolver()
+    {
+        return _resolver;
+    }
+
+    public void setResourceResolver(IResourceResolver resolver)
+    {
+        _resolver = resolver;
+    }
+
+    /**
+     *  Returns the extensions map.
+     *  @return Map of objects.
+     * 
+     **/
+
+    protected Map getExtensions()
+    {
+        return _extensions;
+    }
+
+    /**
+     *  Updates the extension map.
+     *  @param extension A Map of extension specification paths
+     *  keyed on extension id.
+     * 
+     * <p>The map is retained, not copied.
+     *
+     **/
+
+    protected void setExtensions(Map extension)
+    {
+        _extensions = extension;
+    }
+
+    /**
+     *  Returns the libraries map.
+     *  @return Map of {@link LibrarySpecification}.
+     * 
+     **/
+
+    protected Map getLibraries()
+    {
+        return _libraries;
+    }
+
+    /**
+     *  Updates the library map.
+     *  @param libraries A Map of library specification paths 
+     *  keyed on library id.
+     * 
+     *  <p>The map is retained, not copied.
+     *
+     **/
+
+    protected void setLibraries(Map libraries)
+    {
+        _libraries = libraries;
+    }
+
+    /**
+     *  Returns the pages map.
+     *  @return Map of {@link IComponentSpecification}.
+     * 
+     **/
+
+    protected Map getPages()
+    {
+        return _pages;
+    }
+
+    /**
+     *  Updates the page map.
+     *  @param pages A Map of page specification paths 
+     *  keyed on page id.
+     * 
+     *  <p>The map is retained, not copied.
+     *
+     **/
+
+    protected void setPages(Map pages)
+    {
+        _pages = pages;
+    }
+
+    /**
+     * Returns the services.
+     * @return Map of service class names.
+     * 
+     **/
+
+    protected Map getServices()
+    {
+        return _services;
+    }
+
+    /**
+     *  Updates the services map.
+     *  @param services A Map of the fully qualified names of classes 
+     *  which implement
+     *  {@link org.apache.tapestry.engine.IEngineService}
+     *  keyed on service id.
+     * 
+     *  <p>The map is retained, not copied.
+     *
+     **/
+
+    protected void setServices(Map services)
+    {
+        _services = services;
+    }
+
+    /**
+     *  Returns the components map.
+     *  @return Map of {@link IContainedComponent}.
+     * 
+     **/
+
+    protected Map getComponents()
+    {
+        return _components;
+    }
+
+    /**
+     *  Updates the components map.
+     *  @param components A Map of {@link IContainedComponent} keyed on component id.
+     *  The map is retained, not copied.
+     *
+     **/
+
+    protected void setComponents(Map components)
+    {
+        _components = components;
+    }
+
+    /**
+     *  Returns the XML Public Id for the library file, or null
+     *  if not applicable.
+     * 
+     *  <p>
+     *  This method exists as a convienience for the Spindle plugin.
+     *  A previous method used an arbitrary version string, the
+     *  public id is more useful and less ambiguous.
+     *  
+     * 
+     **/
+
+    public String getPublicId()
+    {
+        return _publicId;
+    }
+
+    public void setPublicId(String publicId)
+    {
+        _publicId = publicId;
+    }
+
+    /** @since 3.0 **/
+
+    public IResourceLocation getSpecificationLocation()
+    {
+        return _specificationLocation;
+    }
+
+    /** @since 3.0 **/
+
+    public void setSpecificationLocation(IResourceLocation specificationLocation)
+    {
+        _specificationLocation = specificationLocation;
+    }
+
+    /** @since 3.0 **/
+
+    public synchronized String toString()
+    {
+        ToStringBuilder builder = new ToStringBuilder(this);
+
+        builder.append("components", _components);
+        builder.append("description", _description);
+        builder.append("instantiatedExtensions", _instantiatedExtensions);
+        builder.append("libraries", _libraries);
+        builder.append("pages", _pages);
+        builder.append("publicId", _publicId);
+        builder.append("resolver", _resolver);
+        builder.append("services", _services);
+        builder.append("specificationLocation", _specificationLocation);
+
+        extendDescription(builder);
+
+        return builder.toString();
+    }
+
+    /**
+     *  Does nothing, subclasses may override to add additional
+     *  description.
+     * 
+     *  @see #toString()
+     *  @since 3.0
+     * 
+     **/
+
+    protected void extendDescription(ToStringBuilder builder)
+    {
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/ListenerBindingSpecification.java b/tapestry-framework/src/org/apache/tapestry/spec/ListenerBindingSpecification.java
new file mode 100644
index 0000000..a5fd468
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/ListenerBindingSpecification.java
@@ -0,0 +1,56 @@
+//  Copyright 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.tapestry.spec;
+
+/**
+ *  Special subclass of {@link org.apache.tapestry.spec.BindingSpecification} used
+ *  to encapsulate the additional information 
+ *  specific to listener bindings.  In a ListenerBindingSpecification, the
+ *  value property is the actual script (and is aliased as property script), 
+ *  but an additional property,
+ *  language, (which may be null) is needed.  This is the language
+ *  the script is written in.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class ListenerBindingSpecification extends BindingSpecification implements IListenerBindingSpecification
+{
+    protected String _language;
+    
+    public ListenerBindingSpecification()
+    {
+    	setType(BindingType.LISTENER);
+    }
+    
+    public String getLanguage()
+    {
+        return _language;
+    }
+    
+    public String getScript()
+    {
+        return getValue();
+    }
+    
+    public void setLanguage(String language)
+    {
+        _language = language;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/LocatablePropertyHolder.java b/tapestry-framework/src/org/apache/tapestry/spec/LocatablePropertyHolder.java
new file mode 100644
index 0000000..cbf7f5f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/LocatablePropertyHolder.java
@@ -0,0 +1,47 @@
+//  Copyright 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.tapestry.spec;
+
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.ILocationHolder;
+import org.apache.tapestry.util.BasePropertyHolder;
+
+/**
+ *  Base class for implementing both
+ *  interfaces {@link org.apache.tapestry.util.IPropertyHolder} and
+ *  {@link org.apache.tapestry.ILocationHolder}.  This is
+ *  used by all the specification classes.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class LocatablePropertyHolder extends BasePropertyHolder implements ILocationHolder
+{
+	private ILocation _location;
+	
+    public ILocation getLocation()
+    {
+        return _location;
+    }
+
+    public void setLocation(ILocation location)
+    {
+        _location = location;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/ParameterSpecification.java b/tapestry-framework/src/org/apache/tapestry/spec/ParameterSpecification.java
new file mode 100644
index 0000000..0115043
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/ParameterSpecification.java
@@ -0,0 +1,164 @@
+//  Copyright 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.tapestry.spec;
+
+/**
+ *  Defines a formal parameter to a component.  A <code>IParameterSpecification</code>
+ *  is contained by a {@link IComponentSpecification}.
+ *
+ *  <p>TBD: Identify arrays in some way.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class ParameterSpecification extends BaseLocatable implements IParameterSpecification
+{
+    private boolean required = false;
+    private String type;
+
+    /** @since 1.0.9 **/
+    private String description;
+    
+    /** @since 2.0.3 **/
+    private String propertyName;
+    
+    /** @since 3.0 **/
+    private String defaultValue = null;
+
+	private Direction direction = Direction.CUSTOM;
+	
+    /**
+     *  Returns the class name of the expected type of the parameter.  The default value
+     *  is <code>java.lang.Object</code> which matches anything.
+     *
+     **/
+
+    public String getType()
+    {
+        return type;
+    }
+
+    /**
+     *  Returns true if the parameter is required by the component.
+     *  The default is false, meaning the parameter is optional.
+     *
+     **/
+
+    public boolean isRequired()
+    {
+        return required;
+    }
+
+    public void setRequired(boolean value)
+    {
+        required = value;
+    }
+
+	/**
+	 *  Sets the type of value expected for the parameter.  This can be
+	 *  left blank to indicate any type.
+	 * 
+	 **/
+	
+    public void setType(String value)
+    {
+        type = value;
+    }
+
+    /**
+     *  Returns the documentation for this parameter.
+     * 
+     *  @since 1.0.9
+     * 
+     **/
+
+    public String getDescription()
+    {
+        return description;
+    }
+
+    /**
+     *  Sets the documentation for this parameter.
+     * 
+     *  @since 1.0.9
+     *    	 
+     **/
+
+    public void setDescription(String description)
+    {
+        this.description = description;
+    }
+    
+    /**
+     *  Sets the property name (of the component class)
+     *  to connect the parameter to.
+     * 
+     **/
+    
+    public void setPropertyName(String propertyName)
+    {
+        this.propertyName = propertyName;
+    }
+    
+    /**
+     *  Returns the name of the JavaBeans property to connect the
+     *  parameter to.
+     * 
+     **/
+    
+    public String getPropertyName()
+    {
+       return propertyName;
+    }
+
+	/**
+	 *  Returns the parameter value direction, defaulting to {@link Direction#CUSTOM}
+	 *  if not otherwise specified.
+	 * 
+	 **/
+	
+    public Direction getDirection()
+    {
+        return direction;
+    }
+
+    public void setDirection(Direction direction)
+    {
+        if (direction == null)
+        	throw new IllegalArgumentException("direction may not be null.");
+        	
+        this.direction = direction;
+    }
+
+
+    /**
+     * @see org.apache.tapestry.spec.IParameterSpecification#getDefaultValue()
+     */
+    public String getDefaultValue()
+    {
+        return defaultValue;
+    }
+
+    /**
+     * @see org.apache.tapestry.spec.IParameterSpecification#setDefaultValue(java.lang.String)
+     */
+    public void setDefaultValue(String defaultValue)
+    {
+        this.defaultValue = defaultValue;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/PropertySpecification.java b/tapestry-framework/src/org/apache/tapestry/spec/PropertySpecification.java
new file mode 100644
index 0000000..566a364
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/PropertySpecification.java
@@ -0,0 +1,81 @@
+//  Copyright 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.tapestry.spec;
+/**
+ *  Defines a transient or persistant property of a component or page.  
+ *  A {@link org.apache.tapestry.engine.IComponentClassEnhancer} uses this information
+ *  to create a subclass with the necessary instance variables and methods.  
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ * 
+ **/
+
+public class PropertySpecification extends BaseLocatable implements IPropertySpecification
+{
+	private String _name;
+	private String _type = "java.lang.Object";
+	private boolean _persistent;
+	private String _initialValue;
+	
+    public String getInitialValue()
+    {
+        return _initialValue;
+    }
+
+    public String getName()
+    {
+        return _name;
+    }
+
+    public boolean isPersistent()
+    {
+        return _persistent;
+    }
+
+    public String getType()
+    {
+        return _type;
+    }
+
+    public void setInitialValue(String initialValue)
+    {
+        _initialValue = initialValue;
+    }
+
+	/**
+	 *  Sets the name of the property.  This should not be changed
+	 *  once this IPropertySpecification is added to
+	 *  a {@link org.apache.tapestry.spec.ComponentSpecification}.
+	 * 
+	 **/
+	
+    public void setName(String name)
+    {
+        _name = name;
+    }
+
+    public void setPersistent(boolean persistant)
+    {
+        _persistent = persistant;
+    }
+
+    public void setType(String type)
+    {
+        _type = type;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/SpecFactory.java b/tapestry-framework/src/org/apache/tapestry/spec/SpecFactory.java
new file mode 100644
index 0000000..2210b0a
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/SpecFactory.java
@@ -0,0 +1,180 @@
+//  Copyright 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.tapestry.spec;
+
+import org.apache.tapestry.bean.ExpressionBeanInitializer;
+import org.apache.tapestry.bean.IBeanInitializer;
+import org.apache.tapestry.bean.MessageBeanInitializer;
+
+/**
+ *  A Factory used by {@link org.apache.tapestry.parse.SpecificationParser} to create Tapestry
+ *  domain objects.
+ * 
+ *  <p>
+ *  The default implementation here creates the expected runtime
+ *  instances of classes in packages:
+ *  <ul>
+ *  <li>org.apache.tapestry.spec</li>
+ *  <li>org.apache.tapestry.bean</li>
+ *  </ul>
+ * 
+ *  <p>
+ *  This class is extended by Spindle - the Eclipse Plugin for Tapestry
+ * 
+ *  @author GWL
+ *  @since 1.0.9
+ * 
+ **/
+
+public class SpecFactory
+{
+    /**
+     * Creates a concrete instance of {@link ApplicationSpecification}.
+     **/
+
+    public IApplicationSpecification createApplicationSpecification()
+    {
+        return new ApplicationSpecification();
+    }
+
+    /**
+     *  Creates an instance of {@link LibrarySpecification}.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public ILibrarySpecification createLibrarySpecification()
+    {
+        return new LibrarySpecification();
+    }
+
+	/**
+	 *  Returns a new instance of {@link IAssetSpecification}.
+	 * 
+	 *  @since 3.0
+	 * 
+	 **/
+	
+	public IAssetSpecification createAssetSpecification()
+	{
+		return new AssetSpecification();
+	}
+
+	/**
+	 *  Creates a new instance of {@link IBeanSpecification}.
+	 * 
+	 *  @since 3.0
+	 * 
+	 **/
+	
+	public IBeanSpecification createBeanSpecification()
+	{
+		return new BeanSpecification();
+	}
+
+	public IBindingSpecification createBindingSpecification() 
+	{
+		return new BindingSpecification();
+	}
+
+    /**
+     *  Creates a new concrete instance of {@link IListenerBindingSpecification} for the
+     *  given language (which is option) and script.
+     * 
+     *  @since 3.0
+     *  
+     **/
+
+    public IListenerBindingSpecification createListenerBindingSpecification()
+    {
+        return new ListenerBindingSpecification();
+    }
+
+    /**
+     * Creates a concrete instance of {@link IComponentSpecification}.
+     **/
+
+    public IComponentSpecification createComponentSpecification()
+    {
+        return new ComponentSpecification();
+    }
+
+    /**
+     * Creates a concrete instance of {@link IContainedComponent}.
+     **/
+
+    public IContainedComponent createContainedComponent()
+    {
+        return new ContainedComponent();
+    }
+
+    /**
+     * Creates a concrete instance of {@link ParameterSpecification}.
+     **/
+
+    public IParameterSpecification createParameterSpecification()
+    {
+        return new ParameterSpecification();
+    }
+
+	/**
+	 *  Creates a new instance of {@link ExpressionBeanInitializer}.
+	 * 
+	 *  @since 3.0
+	 * 
+	 **/
+	
+	public IBeanInitializer createExpressionBeanInitializer()
+	{
+		return new ExpressionBeanInitializer();
+	}
+
+	/**
+	 *  Returns a new instance of {@link MessageBeanInitializer}.
+	 * 
+	 *  @since 3.0
+	 * 
+	 **/
+	
+	public IBeanInitializer createMessageBeanInitializer()
+	{
+		return new MessageBeanInitializer();
+	}
+
+    /**
+     *  Creates a concrete instance of {@link org.apache.tapestry.spec.IExtensionSpecification}.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public IExtensionSpecification createExtensionSpecification()
+    {
+        return new ExtensionSpecification();
+    }
+    
+    /**
+     *  Creates a concrete instance of {@link org.apache.tapestry.spec.IPropertySpecification}.
+     * 
+     *  @since 3.0
+     * 
+     **/
+    
+    public IPropertySpecification createPropertySpecification()
+    {
+    	return new PropertySpecification();
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/spec/package.html b/tapestry-framework/src/org/apache/tapestry/spec/package.html
new file mode 100644
index 0000000..2dfaa02
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/spec/package.html
@@ -0,0 +1,14 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+<p>Classes to represent application and component specifications.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/util/AdaptorRegistry.java b/tapestry-framework/src/org/apache/tapestry/util/AdaptorRegistry.java
new file mode 100644
index 0000000..01ad478
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/AdaptorRegistry.java
@@ -0,0 +1,319 @@
+//  Copyright 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.tapestry.util;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  An implementation of the <b>Adaptor</b> pattern.  The adaptor
+ *  pattern allows new functionality to be assigned to an existing class.
+ *  As implemented here, this is a smart lookup between
+ *  a particular class (the class to be adapted, called
+ *  the <em>subject class</em>) and some object instance
+ *  that can provide the extra functionality (called the
+ *  <em>adaptor</em>).  The implementation of the adaptor is not relevant
+ *  to the AdaptorRegistry class.
+ *
+ *  <p>Adaptors are registered before they can be used; the registration maps a
+ *  particular class to an adaptor instance.  The adaptor instance will be used
+ *  when the subject class matches the registered class, or the subject class
+ *  inherits from the registered class.
+ *
+ *  <p>This means that a search must be made that walks the inheritance tree
+ *  (upwards from the subject class) to find a registered mapping.
+ *
+ *  <p>In addition, adaptors can be registered against <em>interfaces</em>.
+ *  Searching of interfaces occurs after searching of classes.  The exact order is:
+ *
+ *  <ul>
+ *  <li>Search for the subject class, then each super-class of the subject class
+ *      (excluding java.lang.Object)
+ *  <li>Search interfaces, starting with interfaces implemented by the subject class,
+ *  continuing with interfaces implemented by the super-classes, then
+ *  interfaces extended by earlier interfaces (the exact order is a bit fuzzy)
+ *  <li>Search for a match for java.lang.Object, if any
+ *  </ul>
+ *
+ *  <p>The first match terminates the search.
+ *
+ *  <p>The AdaptorRegistry caches the results of search; a subsequent search for the
+ *  same subject class will be resolved immediately.
+ * 
+ *  <p>AdaptorRegistry does a minor tweak of the "natural" inheritance.
+ *  Normally, the parent class of an object array (i.e., <code>Foo[]</code>) is
+ *  simply <code>Object</code>, even though you may assign 
+ *  <code>Foo[]</code> to a variable of type <code>Object[]</code>.  AdaptorRegistry
+ *  "fixes" this by searching for <code>Object[]</code> as if it was the superclass of
+ *  any object array.  This means that the search path for <code>Foo[]</code> is
+ *  <code>Foo[]</code>, <code>Object[]</code>, then a couple of interfaces 
+ *  {@link java.lang.Cloneable}, {@link java.io.Serializable}, etc. that are\
+ *  implicitily implemented by arrarys), and then, finally, <code>Object</code>
+ * 
+ *  <p>
+ *  This tweak doesn't apply to scalar arrays, since scalar arrays may <em>not</em>
+ *  be assigned to <code>Object[]</code>. 
+ *
+ *  <p>This class is thread safe.
+ *
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ * 
+ **/
+
+public class AdaptorRegistry
+{
+    private static final Log LOG = LogFactory.getLog(AdaptorRegistry.class);
+
+    /**
+     *  A Map of adaptor objects, keyed on registration Class.
+     *
+     **/
+
+    private Map registrations = new HashMap();
+
+    /**
+     *  A Map of adaptor objects, keyed on subject Class.
+     *
+     **/
+
+    private Map cache = new HashMap();
+
+    /**
+     *  Registers an adaptor for a registration class.
+     *
+     *  @throws IllegalArgumentException if an adaptor has already
+     *  been registered for the given class.
+     **/
+
+    public synchronized void register(Class registrationClass, Object adaptor)
+    {
+        if (registrations.containsKey(registrationClass))
+            throw new IllegalArgumentException(
+                Tapestry.format(
+                    "AdaptorRegistry.duplicate-registration",
+                    Tapestry.getClassName(registrationClass)));
+
+        registrations.put(registrationClass, adaptor);
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Registered " + adaptor + " for " + Tapestry.getClassName(registrationClass));
+
+        // Can't tell what is and isn't valid in the cache.
+        // Also, normally all registrations occur before any adaptors
+        // are searched for, so this is not a big deal.
+
+        cache.clear();
+    }
+
+    /**
+     *  Gets the adaptor for the specified subjectClass.
+     *
+     *  @throws IllegalArgumentException if no adaptor could be found.
+     *
+     **/
+
+    public synchronized Object getAdaptor(Class subjectClass)
+    {
+        Object result;
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Getting adaptor for class " + Tapestry.getClassName(subjectClass));
+
+        result = cache.get(subjectClass);
+
+        if (result != null)
+        {
+            if (LOG.isDebugEnabled())
+                LOG.debug("Found " + result + " in cache");
+
+            return result;
+        }
+
+        result = searchForAdaptor(subjectClass);
+
+        // Record the result in the cache
+
+        cache.put(subjectClass, result);
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Found " + result);
+
+        return result;
+    }
+
+    /**
+     * Searches the registration Map for a match, based on inheritance.
+     *
+     * <p>Searches class inheritance first, then interfaces (in a rather vague order).
+     * Really should match the order from the JVM spec.
+     *
+     * <p>There's a degenerate case where we may check the same interface more than once:
+     * <ul>
+     * <li>Two interfaces, I1 and I2
+     * <li>Two classes, C1 and C2
+     * <li>I2 extends I1
+     * <li>C2 extends C1
+     * <li>C1 implements I1
+     * <li>C2 implements I2
+     * <li>The search will be: C2, C1, I2, I1, I1
+     * <li>I1 is searched twice, because C1 implements it, and I2 extends it
+     * <li>There are other such cases, but none of them cause infinite loops
+     * and most are rare (we could guard against it, but its relatively expensive).
+     * <li>Multiple checks only occur if we don't find a registration
+     * </ul>
+     *
+     *  <p>
+     *  This method is only called from a synchronized block, so it is
+     *  implicitly synchronized.
+     * 
+     **/
+
+    private Object searchForAdaptor(Class subjectClass)
+    {
+        LinkedList queue = null;
+        Object result = null;
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Searching for adaptor for class " + Tapestry.getClassName(subjectClass));
+
+        // Step one: work up through the class inheritance.
+
+        Class searchClass = subjectClass;
+
+        // Primitive types have null, not Object, as their parent
+        // class.
+
+        while (searchClass != Object.class && searchClass != null)
+        {
+            result = registrations.get(searchClass);
+            if (result != null)
+                return result;
+
+            // Not an exact match.  If the search class
+            // implements any interfaces, add them to the queue.
+
+            Class[] interfaces = searchClass.getInterfaces();
+            int length = interfaces.length;
+
+            if (queue == null && length > 0)
+                queue = new LinkedList();
+
+            for (int i = 0; i < length; i++)
+                queue.addLast(interfaces[i]);
+
+            // Advance up to the next superclass
+
+            searchClass = getSuperclass(searchClass);
+
+        }
+
+        // Ok, the easy part failed, lets start searching
+        // interfaces.
+
+        if (queue != null)
+        {
+            while (!queue.isEmpty())
+            {
+                searchClass = (Class) queue.removeFirst();
+
+                result = registrations.get(searchClass);
+                if (result != null)
+                    return result;
+
+                // Interfaces can extend other interfaces; add them
+                // to the queue.
+
+                Class[] interfaces = searchClass.getInterfaces();
+                int length = interfaces.length;
+
+                for (int i = 0; i < length; i++)
+                    queue.addLast(interfaces[i]);
+            }
+        }
+
+        // Not a match on interface; our last gasp is to check
+        // for a registration for java.lang.Object
+
+        result = registrations.get(Object.class);
+        if (result != null)
+            return result;
+
+        // No match?  That's rare ... and an error.
+
+        throw new IllegalArgumentException(
+            Tapestry.format(
+                "AdaptorRegistry.adaptor-not-found",
+                Tapestry.getClassName(subjectClass)));
+    }
+
+    /**
+     *  Returns the superclass of the given class, with a single tweak:  If the 
+     *  search class is an array class, and the component type is an object class
+     *  (but not Object), then the simple Object array class is returned.  This reflects
+     *  the fact that an array of any class may be assignable to <code>Object[]</code>,
+     *  even though the superclass of an array is always simply <code>Object</code>.
+     * 
+     **/
+
+    private Class getSuperclass(Class searchClass)
+    {
+        if (searchClass.isArray())
+        {
+            Class componentType = searchClass.getComponentType();
+
+            if (!componentType.isPrimitive() && componentType != Object.class)
+                return Object[].class;
+        }
+
+        return searchClass.getSuperclass();
+    }
+
+    public synchronized String toString()
+    {
+        StringBuffer buffer = new StringBuffer();
+        buffer.append("AdaptorRegistry[");
+
+        Iterator i = registrations.entrySet().iterator();
+        boolean showSep = false;
+
+        while (i.hasNext())
+        {
+            if (showSep)
+                buffer.append(' ');
+
+            Map.Entry entry = (Map.Entry) i.next();
+
+            Class registeredClass = (Class) entry.getKey();
+
+            buffer.append(Tapestry.getClassName(registeredClass));
+            buffer.append("=");
+            buffer.append(entry.getValue());
+
+            showSep = true;
+        }
+
+        buffer.append("]");
+
+        return buffer.toString();
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/BasePropertyHolder.java b/tapestry-framework/src/org/apache/tapestry/util/BasePropertyHolder.java
new file mode 100644
index 0000000..35746ea
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/BasePropertyHolder.java
@@ -0,0 +1,79 @@
+//  Copyright 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.tapestry.util;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ *  Base class implementation for the {@link IPropertyHolder} interface.
+ *
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public class BasePropertyHolder implements IPropertyHolder
+{
+    private static final int MAP_SIZE = 7;
+    private Map properties;
+
+    public String getProperty(String name)
+    {
+        if (properties == null)
+            return null;
+
+        return (String) properties.get(name);
+    }
+
+    public void setProperty(String name, String value)
+    {
+        if (value == null)
+        {
+            removeProperty(name);
+            return;
+        }
+
+        if (properties == null)
+            properties = new HashMap(MAP_SIZE);
+
+        properties.put(name, value);
+    }
+
+    public void removeProperty(String name)
+    {
+        if (properties == null)
+            return;
+
+        properties.remove(name);
+    }
+
+    public List getPropertyNames()
+    {
+        if (properties == null)
+            return Collections.EMPTY_LIST;
+
+        List result = new ArrayList(properties.keySet());
+        
+        Collections.sort(result);
+        
+        return result;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/ComponentAddress.java b/tapestry-framework/src/org/apache/tapestry/util/ComponentAddress.java
new file mode 100644
index 0000000..38e2818
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/ComponentAddress.java
@@ -0,0 +1,145 @@
+//  Copyright 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.tapestry.util;
+
+import java.io.Serializable;
+
+import org.apache.tapestry.IComponent;
+import org.apache.tapestry.INamespace;
+import org.apache.tapestry.IPage;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ * The ComponentAddress class contains the path to a component, allowing it to  
+ * locate an instance of that component in a different 
+ * {@link org.apache.tapestry.IRequestCycle}.
+ * 
+ * <p>This class needs to be used mostly when working with components
+ * accessed via the {@link org.apache.tapestry.IRender} interface. 
+ * It allows those components to serialize and
+ * pass as a service parameter information about what component they have to 
+ * talk to if control returns back to them. 
+ * 
+ * <p>This situation often occurs when the component used via IRender contains
+ * Direct or Action links.
+ * 
+ * @version $Id$
+ * @author mindbridge
+ * @since 2.2
+ * 
+ */
+public class ComponentAddress implements Serializable
+{
+    private String _pageName;
+    private String _idPath;
+
+    /**
+     * Creates a new ComponentAddress object that carries the identification 
+     * information of the given component (the page name and the ID path).
+     * @param component the component to get the address of
+     */
+    public ComponentAddress(IComponent component)
+    {
+        this(component.getPage().getPageName(), component.getIdPath());
+    }
+
+    /**
+     * Creates a new ComponentAddress using the given Page Name and ID Path
+     * @param pageName the name of the page that contains the component
+     * @param idPath the ID Path of the component
+     */
+    public ComponentAddress(String pageName, String idPath)
+    {
+        _pageName = pageName;
+        _idPath = idPath;
+    }
+
+    /**
+     * Creates a new ComponentAddress using the given Page Name and ID Path
+     * relative on the provided Namespace
+     * @param namespace the namespace of the page that contains the component
+     * @param pageName the name of the page that contains the component
+     * @param idPath the ID Path of the component
+     */
+    public ComponentAddress(
+        INamespace namespace,
+        String pageName,
+        String idPath)
+    {
+        this(namespace.constructQualifiedName(pageName), idPath);
+    }
+
+    /**
+     * Finds a component with the current address using the given RequestCycle.
+     * @param cycle the RequestCycle to use to locate the component
+     * @return IComponent a component that has been initialized for the given RequestCycle
+     */
+    public IComponent findComponent(IRequestCycle cycle)
+    {
+        IPage objPage = cycle.getPage(_pageName);
+        return objPage.getNestedComponent(_idPath);
+    }
+
+    /**
+     * Returns the idPath of the component.
+     * @return String the ID path of the component
+     */
+    public String getIdPath()
+    {
+        return _idPath;
+    }
+
+    /**
+     * Returns the Page Name of the component.
+     * @return String the Page Name of the component
+     */
+    public String getPageName()
+    {
+        return _pageName;
+    }
+
+    /**
+     * @see java.lang.Object#hashCode()
+     */
+    public int hashCode()
+    {
+        int hash = _pageName.hashCode() * 31;
+        if (_idPath != null)
+            hash += _idPath.hashCode();
+        return hash;
+    }
+
+    /**
+     * @see java.lang.Object#equals(Object)
+     */
+    public boolean equals(Object obj)
+    {
+        if (!(obj instanceof ComponentAddress))
+            return false;
+
+        if (obj == this)
+            return true;
+
+        ComponentAddress objAddress = (ComponentAddress) obj;
+        if (!getPageName().equals(objAddress.getPageName()))
+            return false;
+
+        String idPath1 = getIdPath();
+        String idPath2 = objAddress.getIdPath();
+        return (idPath1 == idPath2)
+            || (idPath1 != null && idPath1.equals(idPath2));
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/ContentType.java b/tapestry-framework/src/org/apache/tapestry/util/ContentType.java
new file mode 100644
index 0000000..bafb558
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/ContentType.java
@@ -0,0 +1,189 @@
+//  Copyright 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.tapestry.util;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+/**
+ *  Represents an HTTP content type. Allows to set various elements like
+ *  the mime type, the character set, and other parameters.
+ *  This is similar to a number of other implementations of the same concept in JAF, etc.
+ *  We have created this simple implementation to avoid including the whole libraries. 
+ * 
+ *  @version $Id$
+ *  @author mindbridge
+ *  @since 3.0
+ **/
+public class ContentType
+{
+    private String _baseType;
+    private String _subType;
+    private Map _parameters;
+
+    /**
+     * Creates a new empty content type
+     */
+    public ContentType()
+    {
+        initialize();
+    }
+    
+    /**
+     * Creates a new content type from the argument.
+     * The format of the argument has to be basetype/subtype(;key=value)* 
+     * 
+     * @param contentType the content type that needs to be represented
+     */
+    public ContentType(String contentType)
+    {
+        this();
+        parse(contentType);
+    }
+    
+    private void initialize()
+    {
+        _baseType = "";
+        _subType = "";
+        _parameters = new HashMap();
+    }
+    
+    /**
+     * @return the base type of the content type
+     */
+    public String getBaseType()
+    {
+        return _baseType;
+    }
+
+    /**
+     * @param baseType
+     */
+    public void setBaseType(String baseType)
+    {
+        _baseType = baseType;
+    }
+
+    /**
+     * @return the sub-type of the content type
+     */
+    public String getSubType()
+    {
+        return _subType;
+    }
+
+    /**
+     * @param subType
+     */
+    public void setSubType(String subType)
+    {
+        _subType = subType;
+    }
+
+    /**
+     * @return the MIME type of the content type
+     */
+    public String getMimeType()
+    {
+        return _baseType + "/" + _subType;
+    }
+
+    /**
+     * @return the list of names of parameters in this content type 
+     */
+    public String[] getParameterNames()
+    {
+        Set parameterNames = _parameters.keySet(); 
+        return (String[]) parameterNames.toArray(new String[parameterNames.size()]);
+    }
+
+    /**
+     * @param key the name of the content type parameter
+     * @return the value of the content type parameter
+     */
+    public String getParameter(String key)
+    {
+        return (String) _parameters.get(key);
+    }
+
+    /**
+     * @param key the name of the content type parameter
+     * @param value the value of the content type parameter
+     */
+    public void setParameter(String key, String value)
+    {
+        _parameters.put(key.toLowerCase(), value);
+    }
+
+    /**
+     * Parses the argument and configures the content type accordingly.
+     * The format of the argument has to be type/subtype(;key=value)* 
+     * 
+     * @param contentType the content type that needs to be represented
+     */
+    public void parse(String contentType)
+    {
+        initialize();
+
+        StringTokenizer tokens = new StringTokenizer(contentType, ";");
+        if (!tokens.hasMoreTokens()) 
+            return;
+        
+        String mimeType = tokens.nextToken();
+        StringTokenizer mimeTokens = new StringTokenizer(mimeType, "/");
+        setBaseType(mimeTokens.hasMoreTokens() ? mimeTokens.nextToken() : "");
+        setSubType(mimeTokens.hasMoreTokens() ? mimeTokens.nextToken() : "");
+        
+        while (tokens.hasMoreTokens()) {
+            String parameter = tokens.nextToken();
+
+            StringTokenizer parameterTokens = new StringTokenizer(parameter, "=");
+            String key = parameterTokens.hasMoreTokens() ? parameterTokens.nextToken() : "";
+            String value = parameterTokens.hasMoreTokens() ? parameterTokens.nextToken() : "";
+            setParameter(key, value);
+        }
+    }
+
+    
+
+    /**
+     * @return the string representation of this content type
+     */
+    public String unparse()
+    {
+        StringBuffer buf = new StringBuffer(getMimeType());
+
+        String[] parameterNames = getParameterNames();
+        for (int i = 0; i < parameterNames.length; i++)
+        {
+            String key = parameterNames[i];
+            String value = getParameter(key);
+            buf.append(";" + key + "=" + value);
+        } 
+        
+        return buf.toString();
+    }
+    
+    /**
+     * @return the string representation of this content type. Same as unparse().
+     */
+    public String toString()
+    {
+        return unparse();
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/DefaultResourceResolver.java b/tapestry-framework/src/org/apache/tapestry/util/DefaultResourceResolver.java
new file mode 100644
index 0000000..9f04c7c
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/DefaultResourceResolver.java
@@ -0,0 +1,131 @@
+//  Copyright 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.tapestry.util;
+
+import java.net.URL;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Default implementation of {@link org.apache.tapestry.IResourceResolver} based
+ *  around {@link Thread#getContextClassLoader()} (which is set by the
+ *  servlet container).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.3
+ * 
+ **/
+
+public class DefaultResourceResolver implements IResourceResolver
+{
+    private static final Log LOG = LogFactory.getLog(DefaultResourceResolver.class);
+
+    private ClassLoader _loader;
+
+    /**
+     *  Constructs a new instance using
+     *  {@link Thread#getContextClassLoader()}.
+     * 
+     **/
+    
+    public DefaultResourceResolver()
+    {
+        this(Thread.currentThread().getContextClassLoader());
+    }
+
+    public DefaultResourceResolver(ClassLoader loader)
+    {
+        _loader = loader;
+    }
+
+    public URL getResource(String name)
+    {
+        boolean debug = LOG.isDebugEnabled();
+
+        if (debug)
+            LOG.debug("getResource(" + name + ")");
+
+        String stripped = removeLeadingSlash(name);
+
+        URL result = _loader.getResource(stripped);
+
+        if (debug)
+        {
+            if (result == null)
+                LOG.debug("Not found.");
+            else
+                LOG.debug("Found as " + result);
+        }
+
+        return result;
+    }
+
+    private String removeLeadingSlash(String name)
+    {
+        if (name.startsWith("/"))
+            return name.substring(1);
+
+        return name;
+    }
+
+    /**
+     *  Invokes {@link Class#forName(java.lang.String, boolean, java.lang.ClassLoader)}.
+     *  
+     *  @param name the complete class name to locate and load
+     *  @return The loaded class
+     *  @throws ApplicationRuntimeException if loading the class throws an exception
+     *  (typically  {@link ClassNotFoundException} or a security exception)
+     * 
+     **/
+    
+    public Class findClass(String name)
+    {
+        try
+        {
+            return Class.forName(name, true, _loader);
+        }
+        catch (Throwable t)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("ResourceResolver.unable-to-load-class", name, _loader, t.getMessage()),
+                t);
+        }
+    }
+
+    /**
+     * 
+     *  OGNL Support for dynamic class loading.  Simply invokes {@link #findClass(String)}.
+     * 
+     **/
+    
+    public Class classForName(String name, Map map) throws ClassNotFoundException
+    {
+        return findClass(name);
+    }
+
+    /** @since 3.0 **/
+    
+    public ClassLoader getClassLoader()
+    {
+        return _loader;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/DelegatingPropertySource.java b/tapestry-framework/src/org/apache/tapestry/util/DelegatingPropertySource.java
new file mode 100644
index 0000000..0085007
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/DelegatingPropertySource.java
@@ -0,0 +1,82 @@
+//  Copyright 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.tapestry.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.tapestry.engine.IPropertySource;
+
+/**
+ *  An implementation of {@link IPropertySource}
+ *  that delegates to a list of other implementations.  This makes
+ *  it possible to create a search path for property values.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.3
+ *
+ **/
+
+public class DelegatingPropertySource implements IPropertySource
+{
+    private List _sources = new ArrayList();
+    
+    public DelegatingPropertySource()
+    {
+    }
+    
+    public DelegatingPropertySource(IPropertySource delegate)
+    {
+        addSource(delegate);
+    }
+    
+    /**
+     *  Adds another source to the list of delegate property sources.
+     *  This is typically only done during initialization
+     *  of this DelegatingPropertySource.
+     * 
+     **/
+    
+    public void addSource(IPropertySource source)
+    {
+        _sources.add(source);
+    }
+    
+    /**
+     *  Re-invokes the method on each delegate property source, 
+     *  in order, return the first non-null value found.
+     * 
+     **/
+    
+    public String getPropertyValue(String propertyName)
+    {
+        String result = null;
+        int count = _sources.size();
+        
+        for (int i = 0; i < count; i++)
+        {
+            IPropertySource source = (IPropertySource)_sources.get(i);
+            
+            result = source.getPropertyValue(propertyName);
+            
+            if (result != null)
+                break;
+        }
+        
+        return result;        
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/ICleanable.java b/tapestry-framework/src/org/apache/tapestry/util/ICleanable.java
new file mode 100644
index 0000000..62c0585
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/ICleanable.java
@@ -0,0 +1,44 @@
+//  Copyright 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.tapestry.util;
+
+/**
+ *  An interface implemented by objects that can be
+ *  cleaned up, which is to say, can release unneeded
+ *  object references.  This is useful for many classes which
+ *  provide a pooling or caching function.  Over time, 
+ *  some pooled or cached objects may no longer be useful
+ *  to keep and can be released. 
+ *  references to unneeded objects.
+ *  This interface is the bridge between
+ *  the {@link JanitorThread} class and an object that
+ *  wishes to be periodically told to "clean up".
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.5
+ *
+ **/
+
+public interface ICleanable
+{
+    /**
+     *  Invoked periodically by the {@link JanitorThread}
+     *  to perform whatever memory cleanups are reasonable.
+     *
+     **/
+
+    public void executeCleanup();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/IPropertyHolder.java b/tapestry-framework/src/org/apache/tapestry/util/IPropertyHolder.java
new file mode 100644
index 0000000..07df9db
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/IPropertyHolder.java
@@ -0,0 +1,63 @@
+//  Copyright 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.tapestry.util;
+
+import java.util.List;
+
+/**
+ *  An interface that defines an object that can store named propertys.  The names
+ *  and the properties are Strings.
+ *
+ *  @version $Id$
+ *  @author Howard Lewis Ship
+ *
+ **/
+
+public interface IPropertyHolder
+{
+    /**
+     *  Returns a List of Strings, the names of all
+     *  properties held by the receiver.  May return an empty list.
+     *  The List is sorted alphabetically.  The List may be modified
+     *  without affecting this property holder.
+     *
+     *  <p>Prior to release 2.2, this method returned Collection.
+     * 
+     **/
+
+    public List getPropertyNames();
+
+    /**
+     *  Sets a named property.  The new value replaces the existing value, if any.
+     *  Setting a property to null is the same as removing the property.
+     *
+     **/
+
+    public void setProperty(String name, String value);
+
+    /**
+     *  Removes the named property, if present.
+     *
+     **/
+
+    public void removeProperty(String name);
+
+    /**
+     *  Retrieves the named property, or null if the property is not defined.
+     *
+     **/
+
+    public String getProperty(String name);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/IRenderDescription.java b/tapestry-framework/src/org/apache/tapestry/util/IRenderDescription.java
new file mode 100644
index 0000000..6c0211f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/IRenderDescription.java
@@ -0,0 +1,37 @@
+//  Copyright 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.tapestry.util;
+
+import org.apache.tapestry.IMarkupWriter;
+
+/**
+ *  An object which may render a description of itself, which is used in debugging
+ *  (i.e., by the Inspector).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.6
+ * 
+ **/
+
+public interface IRenderDescription
+{
+	/**
+	 *  Object should render a description of itself to the writer.
+	 *
+	 **/
+
+	public void renderDescription(IMarkupWriter writer);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/IdAllocator.java b/tapestry-framework/src/org/apache/tapestry/util/IdAllocator.java
new file mode 100644
index 0000000..d7bbbe0
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/IdAllocator.java
@@ -0,0 +1,90 @@
+//  Copyright 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.tapestry.util;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ *  Used to "uniquify" names within a given context.  A base name
+ *  is passed in, and the return value is the base name, or the base name
+ *  extended with a suffix to make it unique.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class IdAllocator
+{
+    private Map _generatorMap = new HashMap();
+
+    private static class NameGenerator
+    {
+        private String _baseId;
+        private int _index;
+
+        NameGenerator(String baseId)
+        {
+            _baseId = baseId + "$";
+        }
+
+        public String nextId()
+        {
+            return _baseId + _index++;
+        }
+    }
+
+    /**
+     *  Allocates the id.  Repeated calls for the same name will return
+     *  "name", "name_0", "name_1", etc.
+     * 
+     **/
+
+    public String allocateId(String name)
+    {
+        NameGenerator g = (NameGenerator) _generatorMap.get(name);
+        String result = null;
+
+        if (g == null)
+        {
+            g = new NameGenerator(name);
+            result = name;
+        }
+        else
+            result = g.nextId();
+
+        // Handle the degenerate case, where a base name of the form "foo$0" has been
+        // requested.  Skip over any duplicates thus formed.
+        
+        while (_generatorMap.containsKey(result))
+            result = g.nextId();
+
+        _generatorMap.put(result, g);
+
+        return result;
+    }
+    
+    /**
+     *  Clears the allocator, resetting it to freshly allocated state.
+     * 
+     **/
+    
+    public void clear()
+    {
+        _generatorMap.clear();
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/JanitorThread.java b/tapestry-framework/src/org/apache/tapestry/util/JanitorThread.java
new file mode 100644
index 0000000..2967472
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/JanitorThread.java
@@ -0,0 +1,216 @@
+//  Copyright 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.tapestry.util;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.tapestry.Tapestry;
+
+/**
+ * A basic kind of janitor, an object that periodically invokes {@link ICleanable#executeCleanup()}
+ * on a set of objects.
+ * <p>
+ * The JanitorThread holds a <em>weak reference</em> to the objects it operates on.
+ * 
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * @since 1.0.5
+ */
+
+public class JanitorThread extends Thread
+{
+    /**
+     * Default number of seconds between janitor runs, about 30 seconds.
+     */
+
+    public static final long DEFAULT_INTERVAL_MILLIS = 30 * 1024;
+
+    private long interval = DEFAULT_INTERVAL_MILLIS;
+
+    private boolean lockInterval = false;
+
+    private static JanitorThread shared = null;
+
+    /**
+     * A {@link List}of {@link WeakReference}s to {@link ICleanable}instances.
+     */
+
+    private List references = new ArrayList();
+
+    /**
+     * Creates a new daemon Janitor.
+     */
+
+    public JanitorThread()
+    {
+        this(null);
+    }
+
+    /**
+     * Creates new Janitor with the given name. The thread will have minimum priority and be a
+     * daemon.
+     */
+
+    public JanitorThread(String name)
+    {
+        super(name);
+
+        setDaemon(true);
+        setPriority(MIN_PRIORITY);
+    }
+
+    /**
+     * Returns a shared instance of JanitorThread. In most cases, the shared instance should be
+     * used, rather than creating a new instance; the exception being when particular scheduling is
+     * of concern. It is also bad policy to change the sleep interval on the shared janitor (though
+     * nothing prevents this, either).
+     */
+
+    public synchronized static JanitorThread getSharedJanitorThread()
+    {
+        if (shared == null)
+        {
+            shared = new JanitorThread("Shared-JanitorThread");
+            shared.lockInterval = true;
+
+            shared.start();
+        }
+
+        return shared;
+    }
+
+    public long getInterval()
+    {
+        return interval;
+    }
+
+    /**
+     * Updates the property, which may not take effect until the next time the thread finishes
+     * sleeping.
+     * 
+     * @param value
+     *            the interval, in milliseconds, between sweeps.
+     * @throws IllegalStateException
+     *             always, if the receiver is the shared JanitorThread
+     * @throws IllegalArgumentException
+     *             if value is less than 1
+     */
+
+    public void setInterval(long value)
+    {
+        if (lockInterval)
+            throw new IllegalStateException(Tapestry.getMessage("JanitorThread.interval-locked"));
+
+        if (value < 1)
+            throw new IllegalArgumentException(Tapestry
+                    .getMessage("JanitorThread.illegal-interval"));
+
+        interval = value;
+    }
+
+    /**
+     * Adds a new cleanable object to the list of references. Care should be taken that objects are
+     * not added multiple times; they will be cleaned too often.
+     */
+
+    public void add(ICleanable cleanable)
+    {
+        WeakReference reference = new WeakReference(cleanable);
+
+        synchronized (references)
+        {
+            references.add(reference);
+        }
+    }
+
+    /**
+     * Runs through the list of targets and invokes {@link ICleanable#executeCleanup()}on each of
+     * them. {@link WeakReference}s that have been invalidated are weeded out.
+     */
+
+    protected void sweep()
+    {
+        synchronized (references)
+        {
+            Iterator i = references.iterator();
+
+            while (i.hasNext())
+            {
+                WeakReference ref = (WeakReference) i.next();
+
+                ICleanable cleanable = (ICleanable) ref.get();
+
+                if (cleanable == null)
+                    i.remove();
+                else
+                    cleanable.executeCleanup();
+            }
+        }
+    }
+
+    /**
+     * Waits for the next run, by sleeping for the desired period. Returns true if the sleep was
+     * successful, or false if the thread was interrupted (and should shut down).
+     */
+
+    protected void waitForNextPass()
+    {
+        try
+        {
+            sleep(interval);
+        }
+        catch (InterruptedException ex)
+        {
+            interrupt();
+        }
+    }
+
+    /**
+     * Alternates between {@link #waitForNextPass()}and {@link #sweep()}.
+     */
+
+    public void run()
+    {
+        while (!isInterrupted())
+        {
+            waitForNextPass();
+
+            sweep();
+        }
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer = new StringBuffer("JanitorThread@");
+        buffer.append(Integer.toHexString(hashCode()));
+
+        buffer.append("[interval=");
+        buffer.append(interval);
+
+        buffer.append(" count=");
+
+        synchronized (references)
+        {
+            buffer.append(references.size());
+        }
+
+        buffer.append(']');
+
+        return buffer.toString();
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/LocalizedContextResourceFinder.java b/tapestry-framework/src/org/apache/tapestry/util/LocalizedContextResourceFinder.java
new file mode 100644
index 0000000..ee11fbf
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/LocalizedContextResourceFinder.java
@@ -0,0 +1,92 @@
+//  Copyright 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.tapestry.util;
+
+import java.net.MalformedURLException;
+import java.util.Locale;
+
+import javax.servlet.ServletContext;
+
+/**
+ *  Finds localized resources within the web application context.
+ * 
+ *  @see javax.servlet.ServletContext
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class LocalizedContextResourceFinder
+{
+    private ServletContext _context;
+
+    public LocalizedContextResourceFinder(ServletContext context)
+    {
+        _context = context;
+    }
+
+    /**
+     *  Resolves the resource, returning a path representing
+     *  the closest match (with respect to the provided locale).
+     *  Returns null if no match.
+     * 
+     *  <p>The provided path is split into a base path
+     *  and a suffix (at the last period character).  The locale
+     *  will provide different suffixes to the base path
+     *  and the first match is returned.
+     * 
+     **/
+
+    public LocalizedResource resolve(String contextPath, Locale locale)
+    {
+        int dotx = contextPath.lastIndexOf('.');
+        String basePath = null;
+        String suffix = null;
+        // This handles assets without extensions - still allows them to be localized.
+        if (dotx > -1) {
+          basePath = contextPath.substring(0, dotx);
+          suffix = contextPath.substring(dotx);
+        } else {
+          basePath = contextPath;
+          suffix = "";
+        }
+
+        LocalizedNameGenerator generator = new LocalizedNameGenerator(basePath, locale, suffix);
+
+        while (generator.more())
+        {
+            String candidatePath = generator.next();
+
+            if (isExistingResource(candidatePath))
+                return new LocalizedResource(candidatePath, generator.getCurrentLocale());
+        }
+
+        return null;
+    }
+
+    private boolean isExistingResource(String path)
+    {
+        try
+        {
+            return _context.getResource(path) != null;
+        }
+        catch (MalformedURLException ex)
+        {
+            return false;
+        }
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/LocalizedNameGenerator.java b/tapestry-framework/src/org/apache/tapestry/util/LocalizedNameGenerator.java
new file mode 100644
index 0000000..7b4fcc5
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/LocalizedNameGenerator.java
@@ -0,0 +1,208 @@
+//  Copyright 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.tapestry.util;
+
+import java.util.Locale;
+import java.util.NoSuchElementException;
+
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Used in a wide variety of resource searches.  Generates
+ *  a series of name variations from a base name, a 
+ *  {@link java.util.Locale} and an optional suffix.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class LocalizedNameGenerator
+{
+    private int _baseNameLength;
+    private String _suffix;
+    private StringBuffer _buffer;
+    private String _language;
+    private String _country;
+    private String _variant;
+    private int _state;
+    private int _prevState;
+
+    private static final int INITIAL = 0;
+    private static final int LCV = 1;
+    private static final int LC = 2;
+    private static final int LV = 3;
+    private static final int L = 4;
+    private static final int BARE = 5;
+    private static final int EXHAUSTED = 6;
+
+    public LocalizedNameGenerator(String baseName, Locale locale, String suffix)
+    {
+        _baseNameLength = baseName.length();
+
+        if (locale != null)
+        {
+            _language = locale.getLanguage();
+            _country = locale.getCountry();
+            _variant = locale.getVariant();
+        }
+
+        _state = INITIAL;
+        _prevState = INITIAL;
+
+        _suffix = suffix;
+
+        _buffer = new StringBuffer(baseName);
+
+        advance();
+    }
+
+    private void advance()
+    {
+        _prevState = _state;
+
+        while (_state != EXHAUSTED)
+        {
+            _state++;
+
+            switch (_state)
+            {
+                case LCV :
+
+                    if (Tapestry.isBlank(_variant))
+                        continue;
+
+                    return;
+
+                case LC :
+
+                    if (Tapestry.isBlank(_country))
+                        continue;
+
+                    return;
+
+                case LV :
+
+                    // If _country is null, then we've already generated this string
+                    // as state LCV and we can continue directly to state L
+
+                    if (Tapestry.isBlank(_variant) || Tapestry.isBlank(_country))
+                        continue;
+
+                    return;
+
+                case L :
+
+                    if (Tapestry.isBlank(_language))
+                        continue;
+
+                    return;
+
+                default :
+                    return;
+            }
+        }
+    }
+
+    /**
+     *  Returns true if there are more name variants to be
+     *  returned, false otherwise.
+     * 
+     **/
+
+    public boolean more()
+    {
+        return _state != EXHAUSTED;
+    }
+
+    /**
+     *  Returns the next localized variant.
+     * 
+     *  @throws NoSuchElementException if all variants have been
+     *  returned.
+     * 
+     **/
+
+    public String next()
+    {
+        if (_state == EXHAUSTED)
+            throw new NoSuchElementException();
+
+        String result = build();
+
+        advance();
+
+        return result;
+    }
+
+    private String build()
+    {
+        _buffer.setLength(_baseNameLength);
+
+        if (_state == LC || _state == LCV || _state == L)
+        {
+            _buffer.append('_');
+            _buffer.append(_language);
+        }
+
+        // For LV, we want two underscores between language
+        // and variant.
+
+        if (_state == LC || _state == LCV || _state == LV)
+        {
+            _buffer.append('_');
+
+            if (_state != LV)
+                _buffer.append(_country);
+        }
+
+        if (_state == LV || _state == LCV)
+        {
+            _buffer.append('_');
+            _buffer.append(_variant);
+        }
+
+        if (_suffix != null)
+            _buffer.append(_suffix);
+
+        return _buffer.toString();
+    }
+
+    public Locale getCurrentLocale()
+    {
+        switch (_prevState)
+        {
+            case LCV :
+
+                return new Locale(_language, _country, _variant);
+
+            case LC :
+
+                return new Locale(_language, _country, "");
+
+            case LV :
+
+                return new Locale(_language, "", _variant);
+
+            case L :
+
+                return new Locale(_language, "", "");
+
+            default :
+                return null;
+        }
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/LocalizedPropertySource.java b/tapestry-framework/src/org/apache/tapestry/util/LocalizedPropertySource.java
new file mode 100644
index 0000000..6cf64c5
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/LocalizedPropertySource.java
@@ -0,0 +1,107 @@
+//  Copyright 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.tapestry.util;
+
+import java.util.Locale;
+
+import org.apache.tapestry.engine.IPropertySource;
+
+/**
+ *  A PropertySource extending the DelegatingPropertySources and adding the 
+ *  capability of searching for localized versions of the desired property.
+ *  Useful when peoperties related to localization are needed.
+ * 
+ *  @author mindbridge
+ *  @version $Id$
+ *  @since 3.0
+ */
+public class LocalizedPropertySource extends DelegatingPropertySource
+{
+    private Locale _locale;
+    
+    /**
+     *  Creates a LocalizedPropertySource with the default locale
+     */
+    public LocalizedPropertySource()
+    {
+        this(Locale.getDefault());
+    }
+
+    /**
+     *  Creates a LocalizedPropertySource with the provided locale
+     */
+    public LocalizedPropertySource(Locale locale)
+    {
+        super();
+        setLocale(locale);
+    }
+
+    /**
+     *  Creates a LocalizedPropertySource with the default locale and the provided delegate
+     */
+    public LocalizedPropertySource(IPropertySource delegate)
+    {
+        this(Locale.getDefault(), delegate);
+    }
+
+    /**
+     *  Creates a LocalizedPropertySource with the provided locale and delegate
+     */
+    public LocalizedPropertySource(Locale locale, IPropertySource delegate)
+    {
+        super(delegate);
+        setLocale(locale);
+    }
+
+
+    /**
+     * @return the locale currently used 
+     */
+    public Locale getLocale()
+    {
+        return _locale;
+    }
+
+    /**
+     * @param locale the locale currently used
+     */
+    public void setLocale(Locale locale)
+    {
+        _locale = locale;
+    }
+
+    
+    /**
+     *  Examines the properties localized using the provided locale in the order
+     *  of more specific to more general and returns the first that has a value. 
+     *  @see org.apache.tapestry.util.DelegatingPropertySource#getPropertyValue(java.lang.String)
+     */
+    public String getPropertyValue(String propertyName)
+    {
+        LocalizedNameGenerator generator = new LocalizedNameGenerator(propertyName, getLocale(), "");
+
+        while (generator.more())
+        {
+            String candidateName = generator.next();
+
+            String value = super.getPropertyValue(candidateName); 
+            if (value != null)
+                return value;
+        }
+
+        return null;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/LocalizedResource.java b/tapestry-framework/src/org/apache/tapestry/util/LocalizedResource.java
new file mode 100644
index 0000000..d23f10a
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/LocalizedResource.java
@@ -0,0 +1,55 @@
+//  Copyright 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.tapestry.util;
+
+import java.util.Locale;
+
+/**
+ *  Contains the path to a localized resource and the locale for which it has been localized.
+ *  This object is immutable.
+ * 
+ *  @author Mindbridge
+ *  @version $Id$
+ *  @since 3.0
+ */
+public class LocalizedResource
+{
+    private String _resourcePath;
+    private Locale _resourceLocale;
+
+
+    public LocalizedResource(String resourcePath, Locale resourceLocale)
+    {
+        _resourcePath = resourcePath;
+        _resourceLocale = resourceLocale;
+    }
+    
+    /**
+     * @return the locale for which this resource has been localized or null if it has not been localized at all
+     */
+    public Locale getResourceLocale()
+    {
+        return _resourceLocale;
+    }
+
+    /**
+     * @return the path to the localized resource
+     */
+    public String getResourcePath()
+    {
+        return _resourcePath;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/LocalizedResourceFinder.java b/tapestry-framework/src/org/apache/tapestry/util/LocalizedResourceFinder.java
new file mode 100644
index 0000000..751ce03
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/LocalizedResourceFinder.java
@@ -0,0 +1,73 @@
+//  Copyright 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.tapestry.util;
+
+import java.util.Locale;
+
+import org.apache.tapestry.IResourceResolver;
+
+/**
+ *  
+ *  Searches for a localization of a
+ *  particular resource in the classpath (using
+ *  a {@link org.apache.tapestry.IResourceResolver}. 
+ * 
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class LocalizedResourceFinder
+{
+    private IResourceResolver _resolver;
+
+    public LocalizedResourceFinder(IResourceResolver resolver)
+    {
+        _resolver = resolver;
+    }
+
+    /**
+     *  Resolves the resource, returning a path representing
+     *  the closest match (with respect to the provided locale).
+     *  Returns null if no match.
+     * 
+     *  <p>The provided path is split into a base path
+     *  and a suffix (at the last period character).  The locale
+     *  will provide different suffixes to the base path
+     *  and the first match is returned.
+     * 
+     **/
+    
+    public LocalizedResource resolve(String resourcePath, Locale locale)
+    {
+        int dotx = resourcePath.lastIndexOf('.');
+        String basePath = resourcePath.substring(0, dotx);
+        String suffix = resourcePath.substring(dotx);
+
+        LocalizedNameGenerator generator = new LocalizedNameGenerator(basePath, locale, suffix);
+
+        while (generator.more())
+        {
+            String candidatePath = generator.next();
+
+            if (_resolver.getResource(candidatePath) != null)
+                return new LocalizedResource(candidatePath, generator.getCurrentLocale());
+        }
+
+        return null;
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/MultiKey.java b/tapestry-framework/src/org/apache/tapestry/util/MultiKey.java
new file mode 100644
index 0000000..df247a0
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/MultiKey.java
@@ -0,0 +1,234 @@
+//  Copyright 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.tapestry.util;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  A complex key that may be used as an alternative to nested
+ *  {@link java.util.Map}s.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+public class MultiKey implements Externalizable
+{
+    /**
+     *  @since 2.0.4
+     * 
+     **/
+    
+    private static final long serialVersionUID = 4465448607415788806L;
+    
+    private static final int HASH_CODE_UNSET = -1;
+
+    private transient int hashCode = HASH_CODE_UNSET;
+
+    private Object[] keys;
+
+    /**
+     *  Public no-arguments constructor needed to be compatible with
+     *  {@link Externalizable}; this leaves the new MultiKey in a
+     *  non-usable state and shouldn't be used by user code.
+     *
+     **/
+
+    public MultiKey()
+    {
+    }
+
+    /**
+     *  Builds a <code>MultiKey</code> from an array of keys.  If the array is not
+     *  copied, then it must not be modified.
+     * 
+     *  @param keys The components of the key.
+     *  @param makeCopy If true, a copy of the keys is created.  If false,
+     *  the keys are simple retained by the <code>MultiKey</code>.
+     *
+     *  @throws IllegalArgumentException if keys is null, of if the
+     *  first element of keys is null.
+     *
+     **/
+
+    public MultiKey(Object[] keys, boolean makeCopy)
+    {
+        super();
+
+        if (keys == null || keys.length == 0)
+            throw new IllegalArgumentException(Tapestry.getMessage("MultiKey.null-keys"));
+
+        if (keys[0] == null)
+            throw new IllegalArgumentException(Tapestry.getMessage("MultiKey.first-element-may-not-be-null"));
+
+        if (makeCopy)
+        {
+            this.keys = new Object[keys.length];
+            System.arraycopy(keys, 0, this.keys, 0, keys.length);
+        }
+        else
+            this.keys = keys;
+    }
+
+    /**
+     *  Returns true if:
+     *  <ul>
+     *  <li>The other object is a <code>MultiKey</code>
+     *  <li>They have the same number of key elements
+     *  <li>Every element is an exact match or is equal
+     *  </ul>
+     *
+     **/
+
+    public boolean equals(Object other)
+    {
+        int i;
+
+        if (other == null)
+            return false;
+
+        if (keys == null)
+            throw new IllegalStateException(Tapestry.getMessage("MultiKey.no-keys"));
+
+        // Would a hashCode check be worthwhile here?
+
+        try
+        {
+            MultiKey otherMulti = (MultiKey) other;
+
+            if (keys.length != otherMulti.keys.length)
+                return false;
+
+            for (i = 0; i < keys.length; i++)
+            {
+                // On an exact match, continue.  This means that null matches
+                // null.
+
+                if (keys[i] == otherMulti.keys[i])
+                    continue;
+
+                // If either is null, but not both, then
+                // not a match.
+
+                if (keys[i] == null || otherMulti.keys[i] == null)
+                    return false;
+
+                if (!keys[i].equals(otherMulti.keys[i]))
+                    return false;
+
+            }
+
+            // Every key equal.  A match.
+
+            return true;
+        }
+        catch (ClassCastException e)
+        {
+        }
+
+        return false;
+    }
+
+    /**
+     *  Returns the hash code of the receiver, which is computed from all the
+     *  non-null key elements.  This value is computed once and
+     *  then cached, so elements should not change their hash codes 
+     *  once created (note that this
+     *  is the same constraint that would be used if the individual 
+     *  key elements were
+     *  themselves {@link java.util.Map} keys.
+     * 
+     *
+     **/
+
+    public int hashCode()
+    {
+        if (hashCode == HASH_CODE_UNSET)
+        {
+            hashCode = keys[0].hashCode();
+
+            for (int i = 1; i < keys.length; i++)
+            {
+                if (keys[i] != null)
+                    hashCode ^= keys[i].hashCode();
+            }
+        }
+
+        return hashCode;
+    }
+
+    /**
+    *  Identifies all the keys stored by this <code>MultiKey</code>.
+    *
+    **/
+
+    public String toString()
+    {
+        StringBuffer buffer;
+        int i;
+
+        buffer = new StringBuffer("MultiKey[");
+
+        for (i = 0; i < keys.length; i++)
+        {
+            if (i > 0)
+                buffer.append(", ");
+
+            if (keys[i] == null)
+                buffer.append("<null>");
+            else
+                buffer.append(keys[i]);
+        }
+
+        buffer.append(']');
+
+        return buffer.toString();
+    }
+
+    /**
+     *  Writes a count of the keys, then writes each individual key.
+     *
+     **/
+
+    public void writeExternal(ObjectOutput out) throws IOException
+    {
+        out.writeInt(keys.length);
+
+        for (int i = 0; i < keys.length; i++)
+            out.writeObject(keys[i]);
+    }
+
+    /**
+     *  Reads the state previously written by {@link #writeExternal(ObjectOutput)}.
+     *
+     **/
+
+    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
+    {
+        int count;
+
+        count = in.readInt();
+        keys = new Object[count];
+
+        for (int i = 0; i < count; i++)
+            keys[i] = in.readObject();
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/PropertyHolderPropertySource.java b/tapestry-framework/src/org/apache/tapestry/util/PropertyHolderPropertySource.java
new file mode 100644
index 0000000..ec1093f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/PropertyHolderPropertySource.java
@@ -0,0 +1,43 @@
+//  Copyright 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.tapestry.util;
+
+import org.apache.tapestry.engine.IPropertySource;
+
+/**
+ *  Implements the {@link IPropertySource} interface
+ *  for instances that implement {@link org.apache.tapestry.util.IPropertyHolder}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.3
+ *
+ **/
+
+public class PropertyHolderPropertySource implements IPropertySource
+{
+    private IPropertyHolder _holder;
+    
+    public PropertyHolderPropertySource(IPropertyHolder holder)
+    {
+        _holder = holder;
+    }
+
+    public String getPropertyValue(String propertyName)
+    {
+        return _holder.getProperty(propertyName);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/RegexpMatcher.java b/tapestry-framework/src/org/apache/tapestry/util/RegexpMatcher.java
new file mode 100644
index 0000000..1716b9d
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/RegexpMatcher.java
@@ -0,0 +1,123 @@
+//  Copyright 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.tapestry.util;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.oro.text.regex.MalformedPatternException;
+import org.apache.oro.text.regex.Pattern;
+import org.apache.oro.text.regex.PatternCompiler;
+import org.apache.oro.text.regex.PatternMatcher;
+import org.apache.oro.text.regex.Perl5Compiler;
+import org.apache.oro.text.regex.Perl5Matcher;
+import org.apache.tapestry.ApplicationRuntimeException;
+
+/**
+ *  Streamlines the interface to ORO by implicitly constructing the
+ *  necessary compilers and matchers, and by
+ *  caching compiled patterns.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class RegexpMatcher
+{
+    private PatternCompiler _patternCompiler;
+
+    private PatternMatcher _matcher;
+
+    private Map _compiledPatterns = new HashMap();
+
+    private Map _escapedPatternStrings = new HashMap();
+
+    protected Pattern compilePattern(String pattern)
+    {
+        if (_patternCompiler == null)
+            _patternCompiler = new Perl5Compiler();
+
+        try
+        {
+            return _patternCompiler.compile(pattern, Perl5Compiler.SINGLELINE_MASK);
+        }
+        catch (MalformedPatternException ex)
+        {
+            throw new ApplicationRuntimeException(ex);
+        }
+    }
+
+    protected Pattern getCompiledPattern(String pattern)
+    {
+        Pattern result = (Pattern) _compiledPatterns.get(pattern);
+
+        if (result == null)
+        {
+            result = compilePattern(pattern);
+            _compiledPatterns.put(pattern, result);
+        }
+
+        return result;
+    }
+
+    /**
+     *  Clears any previously compiled patterns.
+     * 
+     **/
+
+    public void clear()
+    {
+        _compiledPatterns.clear();
+    }
+
+    protected PatternMatcher getPatternMatcher()
+    {
+        if (_matcher == null)
+            _matcher = new Perl5Matcher();
+
+        return _matcher;
+    }
+
+    public boolean matches(String pattern, String input)
+    {
+        Pattern compiledPattern = getCompiledPattern(pattern);
+
+        return getPatternMatcher().matches(input, compiledPattern);
+    }
+
+    public boolean contains(String pattern, String input)
+    {
+        Pattern compiledPattern = getCompiledPattern(pattern);
+
+        return getPatternMatcher().contains(input, compiledPattern);
+    }
+
+    public String getEscapedPatternString(String pattern)
+    {
+        String result = (String) _escapedPatternStrings.get(pattern);
+
+        if (result == null)
+        {
+            result = Perl5Compiler.quotemeta(pattern);
+
+            _escapedPatternStrings.put(pattern, result);
+        }
+
+        return result;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/ResourceBundlePropertySource.java b/tapestry-framework/src/org/apache/tapestry/util/ResourceBundlePropertySource.java
new file mode 100644
index 0000000..b98b637
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/ResourceBundlePropertySource.java
@@ -0,0 +1,61 @@
+//  Copyright 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.tapestry.util;
+
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+
+import org.apache.tapestry.engine.IPropertySource;
+
+/**
+ *  A property source that is based on a {@link java.util.ResourceBundle}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class ResourceBundlePropertySource implements IPropertySource
+{
+    private ResourceBundle _bundle;
+
+    public ResourceBundlePropertySource(ResourceBundle bundle)
+    {
+        _bundle = bundle;
+    }
+
+    /**
+     *  Gets the value from the bundle by invoking
+     *  {@link ResourceBundle#getString(java.lang.String)}.  If
+     *  the bundle does not contain the key (that is, it it
+     *  throws {@link java.util.MissingResourceException}), then
+     *  null is returned.
+     * 
+     **/
+    
+    public String getPropertyValue(String propertyName)
+    {
+        try
+        {
+            return _bundle.getString(propertyName);
+        }
+        catch (MissingResourceException ex)
+        {
+            return null;
+        }
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/ServletContextPropertySource.java b/tapestry-framework/src/org/apache/tapestry/util/ServletContextPropertySource.java
new file mode 100644
index 0000000..1c6c768
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/ServletContextPropertySource.java
@@ -0,0 +1,52 @@
+//  Copyright 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.tapestry.util;
+
+/**
+ *  Implementation of {@link IPropertySource}
+ *  that returns values defined as ServletContext initialization parameters
+ *  (defined as <code>&lt;init-param&gt;</code> in the
+ *  <code>web.xml</code> deployment descriptor.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.3
+ *
+ **/
+
+import javax.servlet.ServletContext;
+
+import org.apache.tapestry.engine.IPropertySource;
+
+public class ServletContextPropertySource implements IPropertySource
+{
+    private ServletContext _context;
+
+    public ServletContextPropertySource(ServletContext context)
+    {
+        _context = context;
+    }
+
+    /**
+     *  Invokes {@link ServletContext#getInitParameter(java.lang.String)}.
+     *
+     **/
+
+    public String getPropertyValue(String propertyName)
+    {
+        return _context.getInitParameter(propertyName);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/ServletPropertySource.java b/tapestry-framework/src/org/apache/tapestry/util/ServletPropertySource.java
new file mode 100644
index 0000000..d5dca28
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/ServletPropertySource.java
@@ -0,0 +1,52 @@
+//  Copyright 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.tapestry.util;
+
+import javax.servlet.ServletConfig;
+
+import org.apache.tapestry.engine.IPropertySource;
+
+/**
+ *  Implementation of {@link IPropertySource}
+ *  that returns values defined as Servlet initialization parameters
+ *  (defined as <code>&lt;init-param&gt;</code> in the
+ *  <code>web.xml</code> deployment descriptor.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.3
+ *
+ **/ 
+
+public class ServletPropertySource implements IPropertySource
+{
+    private ServletConfig _config;
+    
+    public ServletPropertySource(ServletConfig config)
+    {
+        _config = config;
+    }   
+    
+    /**
+     *  Invokes {@link ServletConfig#getInitParameter(java.lang.String)}.
+     * 
+     **/
+    
+    public String getPropertyValue(String propertyName)
+    {
+        return _config.getInitParameter(propertyName);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/StringSplitter.java b/tapestry-framework/src/org/apache/tapestry/util/StringSplitter.java
new file mode 100644
index 0000000..7e725a8
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/StringSplitter.java
@@ -0,0 +1,124 @@
+//  Copyright 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.tapestry.util;
+
+/**
+ *  Used to split a string into substrings based on a single character
+ *  delimiter.  A fast, simple version of
+ *  {@link java.util.StringTokenizer}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class StringSplitter
+{
+    private char delimiter;
+
+    public StringSplitter(char delimiter)
+    {
+        this.delimiter = delimiter;
+    }
+
+    public char getDelimiter()
+    {
+        return delimiter;
+    }
+
+    /**
+     *  Splits a string on the delimter into an array of String
+     *  tokens.  The delimiters are not included in the tokens.  Null
+     *  tokens (caused by two consecutive delimiter) are reduced to an
+     *  empty string. Leading delimiters are ignored.
+     *
+     **/
+
+    public String[] splitToArray(String value)
+    {
+        char[] buffer;
+        int i;
+        String[] result;
+        int resultCount = 0;
+        int start;
+        int length;
+        String token;
+        String[] newResult;
+        boolean first = true;
+
+        buffer = value.toCharArray();
+
+        result = new String[3];
+
+        start = 0;
+        length = 0;
+
+        for (i = 0; i < buffer.length; i++)
+        {
+            if (buffer[i] != delimiter)
+            {
+                length++;
+                continue;
+            }
+
+            // This is used to ignore leading delimiter(s).
+
+            if (length > 0 || !first)
+            {
+                token = new String(buffer, start, length);
+
+                if (resultCount == result.length)
+                {
+                    newResult = new String[result.length * 2];
+
+                    System.arraycopy(result, 0, newResult, 0, result.length);
+
+                    result = newResult;
+                }
+
+                result[resultCount++] = token;
+
+                first = false;
+            }
+
+            start = i + 1;
+            length = 0;
+        }
+
+        // Special case:  if the string contains no delimiters
+        // then it isn't really split.  Wrap the input string
+        // in an array and return.  This is a little optimization
+        // to prevent a new String instance from being
+        // created unnecessarily.
+
+        if (start == 0 && length == buffer.length)
+        {
+            result = new String[1];
+            result[0] = value;
+            return result;
+        }
+
+        // If the string is all delimiters, then this
+        // will result in a single empty token.
+
+        token = new String(buffer, start, length);
+
+        newResult = new String[resultCount + 1];
+        System.arraycopy(result, 0, newResult, 0, resultCount);
+        newResult[resultCount] = token;
+
+        return newResult;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/SystemPropertiesPropertySource.java b/tapestry-framework/src/org/apache/tapestry/util/SystemPropertiesPropertySource.java
new file mode 100644
index 0000000..315d271
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/SystemPropertiesPropertySource.java
@@ -0,0 +1,51 @@
+//  Copyright 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.tapestry.util;
+
+import org.apache.tapestry.engine.IPropertySource;
+
+/**
+ *  Obtain properties from JVM system properties.
+ * 
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.3
+ *
+ **/
+
+public class SystemPropertiesPropertySource implements IPropertySource
+{
+    private static IPropertySource _shared;
+    
+    public static synchronized IPropertySource getInstance()
+    {
+        if (_shared == null)
+            _shared = new SystemPropertiesPropertySource();
+            
+        return _shared; 
+    }
+
+    /**
+     *  Delegates to {@link System#getProperty(java.lang.String)}.
+     * 
+     **/
+    
+    public String getPropertyValue(String propertyName)
+    {
+        return System.getProperty(propertyName);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/exception/ExceptionAnalyzer.java b/tapestry-framework/src/org/apache/tapestry/util/exception/ExceptionAnalyzer.java
new file mode 100644
index 0000000..4056dc5
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/exception/ExceptionAnalyzer.java
@@ -0,0 +1,435 @@
+//  Copyright 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.tapestry.util.exception;
+
+import java.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.io.CharArrayWriter;
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.StringReader;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *  Analyzes an exception, creating one or more 
+ *  {@link ExceptionDescription}s
+ *  from it.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class ExceptionAnalyzer
+{
+    private List exceptionDescriptions;
+    private List propertyDescriptions;
+    private CharArrayWriter writer;
+
+    private static final int LIST_SIZE = 3;
+
+    private boolean exhaustive = false;
+
+    /**
+     *  If true, then stack trace is extracted for each exception.  If false,
+     *  the default, then stack trace is extracted for only the deepest exception.
+     *
+     **/
+
+    public boolean isExhaustive()
+    {
+        return exhaustive;
+    }
+
+    public void setExhaustive(boolean value)
+    {
+        exhaustive = value;
+    }
+
+    /**
+     *  Analyzes the exceptions.  This builds an {@link ExceptionDescription} for the
+     *  exception.  It also looks for a non-null {@link Throwable}
+     *  property.  If one exists, then a second {@link ExceptionDescription} 
+     *  is created.  This continues until no more nested exceptions can be found.
+     *
+     *  <p>The description includes a set of name/value properties 
+     *  (as {@link ExceptionProperty}) object.  This list contains all
+     *  non-null properties that are not, themselves, {@link Throwable}.
+     *
+     *  <p>The name is the display name (not the logical name) of the property.  The value
+     *  is the <code>toString()</code> value of the property.
+     *
+     *  Only properties defined in subclasses of {@link Throwable} are included.
+     *
+     *  <p>A future enhancement will be to alphabetically sort the properties by name.
+     **/
+
+    public ExceptionDescription[] analyze(Throwable exception)
+    {
+        ExceptionDescription[] result;
+
+        if (writer == null)
+            writer = new CharArrayWriter();
+
+        if (propertyDescriptions == null)
+            propertyDescriptions = new ArrayList(LIST_SIZE);
+
+        if (exceptionDescriptions == null)
+            exceptionDescriptions = new ArrayList(LIST_SIZE);
+
+        while (exception != null)
+        {
+            exception = buildDescription(exception);
+        }
+
+        result = new ExceptionDescription[exceptionDescriptions.size()];
+        result = (ExceptionDescription[]) exceptionDescriptions.toArray(result);
+
+        exceptionDescriptions.clear();
+        propertyDescriptions.clear();
+
+        writer.reset();
+
+        // We never actually close() the writer which is bad ... I'm expecting that
+        // the finalize() method will close them, or that they don't need to
+        // close.
+
+        return result;
+    }
+
+    protected Throwable buildDescription(Throwable exception)
+    {
+        BeanInfo info;
+        Class exceptionClass;
+        ExceptionProperty property;
+        PropertyDescriptor[] descriptors;
+        PropertyDescriptor descriptor;
+        Throwable next = null;
+        int i;
+        Object value;
+        Method method;
+        ExceptionProperty[] properties;
+        ExceptionDescription description;
+        String stringValue;
+        String message;
+        String[] stackTrace = null;
+
+        propertyDescriptions.clear();
+
+        message = exception.getMessage();
+        exceptionClass = exception.getClass();
+
+        // Get properties, ignoring those in Throwable and higher
+        // (including the 'message' property).
+
+        try
+        {
+            info = Introspector.getBeanInfo(exceptionClass, Throwable.class);
+        }
+        catch (IntrospectionException e)
+        {
+            return null;
+        }
+
+        descriptors = info.getPropertyDescriptors();
+
+        for (i = 0; i < descriptors.length; i++)
+        {
+            descriptor = descriptors[i];
+
+            method = descriptor.getReadMethod();
+            if (method == null)
+                continue;
+
+            try
+            {
+                value = method.invoke(exception, null);
+            }
+            catch (Exception e)
+            {
+                continue;
+            }
+
+            if (value == null)
+                continue;
+
+            // Some annoying exceptions duplicate the message property
+            // (I'm talking to YOU SAXParseException), so just edit that out.
+
+            if (message != null && message.equals(value))
+                continue;
+
+            // Skip Throwables ... but the first non-null
+            // found is the next exception.  We kind of count
+            // on there being no more than one Throwable
+            // property per Exception.
+
+            if (value instanceof Throwable)
+            {
+                if (next == null)
+                    next = (Throwable) value;
+
+                continue;
+            }
+
+            stringValue = value.toString().trim();
+
+            if (stringValue.length() == 0)
+                continue;
+
+            property = new ExceptionProperty(descriptor.getDisplayName(), value.toString());
+
+            propertyDescriptions.add(property);
+        }
+
+        // If exhaustive, or in the deepest exception (where there's no next)
+        // the extract the stack trace.
+
+        if (next == null || exhaustive)
+            stackTrace = getStackTrace(exception);
+
+        // Would be nice to sort the properties here.
+
+        properties = new ExceptionProperty[propertyDescriptions.size()];
+
+        ExceptionProperty[] propArray =
+            (ExceptionProperty[]) propertyDescriptions.toArray(properties);
+
+        description =
+            new ExceptionDescription(
+                exceptionClass.getName(),
+                message,
+                propArray,
+                stackTrace);
+
+        exceptionDescriptions.add(description);
+
+        return next;
+    }
+
+    /**
+     *  Gets the stack trace for the exception, and converts it into an array of strings.
+     *
+     *  <p>This involves parsing the 
+     *   string generated indirectly from
+     *  <code>Throwable.printStackTrace(PrintWriter)</code>.  This method can get confused
+     *  if the message (presumably, the first line emitted by printStackTrace())
+     *  spans multiple lines.
+     *
+     *  <p>Different JVMs format the exception in different ways.
+     *
+     *  <p>A possible expansion would be more flexibility in defining the pattern
+     *  used.  Hopefully all 'mainstream' JVMs are close enough for this to continue
+     *  working.
+     *
+     **/
+
+    protected String[] getStackTrace(Throwable exception)
+    {
+        writer.reset();
+
+        PrintWriter printWriter = new PrintWriter(writer);
+
+        exception.printStackTrace(printWriter);
+
+        printWriter.close();
+
+        String fullTrace = writer.toString();
+
+        writer.reset();
+
+        // OK, the trick is to convert the full trace into an array of stack frames.
+
+        StringReader stringReader = new StringReader(fullTrace);
+        LineNumberReader lineReader = new LineNumberReader(stringReader);
+        int lineNumber = 0;
+        List frames = new ArrayList();
+
+        try
+        {
+            while (true)
+            {
+                String line = lineReader.readLine();
+
+                if (line == null)
+                    break;
+
+                // Always ignore the first line.
+
+                if (++lineNumber == 1)
+                    continue;
+
+                frames.add(stripFrame(line));
+            }
+
+            lineReader.close();
+        }
+        catch (IOException ex)
+        {
+            // Not likely to happen with this particular set
+            // of readers.
+        }
+
+        String result[] = new String[frames.size()];
+
+        return (String[]) frames.toArray(result);
+    }
+
+    private static final int SKIP_LEADING_WHITESPACE = 0;
+    private static final int SKIP_T = 1;
+    private static final int SKIP_OTHER_WHITESPACE = 2;
+
+    /**
+     *  Sun's JVM prefixes each line in the stack trace
+     *  with "<tab>at ", other JVMs don't.  This method
+     *  looks for and strips such stuff.
+     *
+     **/
+
+    private String stripFrame(String frame)
+    {
+        char array[] = frame.toCharArray();
+
+        int i = 0;
+        int state = SKIP_LEADING_WHITESPACE;
+        boolean more = true;
+
+        while (more)
+        {
+            // Ran out of characters to skip?  Return the empty string.
+
+            if (i == array.length)
+                return "";
+
+            char ch = array[i];
+
+            switch (state)
+            {
+                // Ignore whitespace at the start of the line.
+
+                case SKIP_LEADING_WHITESPACE :
+
+                    if (Character.isWhitespace(ch))
+                    {
+                        i++;
+                        continue;
+                    }
+
+                    if (ch == 'a')
+                    {
+                        state = SKIP_T;
+                        i++;
+                        continue;
+                    }
+
+                    // Found non-whitespace, not 'a'
+                    more = false;
+                    break;
+
+                    // Skip over the 't' after an 'a'
+
+                case SKIP_T :
+
+                    if (ch == 't')
+                    {
+                        state = SKIP_OTHER_WHITESPACE;
+                        i++;
+                        continue;
+                    }
+
+                    // Back out the skipped-over 'a'
+
+                    i--;
+                    more = false;
+                    break;
+
+                    // Skip whitespace between 'at' and the name of the class
+
+                case SKIP_OTHER_WHITESPACE :
+
+                    if (Character.isWhitespace(ch))
+                    {
+                        i++;
+                        continue;
+                    }
+
+                    // Not whitespace
+                    more = false;
+                    break;
+            }
+
+        }
+
+        // Found nothing to strip out.
+
+        if (i == 0)
+            return frame;
+
+        return frame.substring(i);
+    }
+
+    /**
+     *  Produces a text based exception report to the provided stream.
+     *
+     **/
+
+    public void reportException(Throwable exception, PrintStream stream)
+    {
+        int i;
+        int j;
+        ExceptionDescription[] descriptions;
+        ExceptionProperty[] properties;
+        String[] stackTrace;
+        String message;
+
+        descriptions = analyze(exception);
+
+        for (i = 0; i < descriptions.length; i++)
+        {
+            message = descriptions[i].getMessage();
+
+            if (message == null)
+                stream.println(descriptions[i].getExceptionClassName());
+            else
+                stream.println(
+                    descriptions[i].getExceptionClassName() + ": " + descriptions[i].getMessage());
+
+            properties = descriptions[i].getProperties();
+
+            for (j = 0; j < properties.length; j++)
+                stream.println(
+                    "   " + properties[j].getName() + ": " + properties[j].getValue());
+
+            // Just show the stack trace on the deepest exception.
+
+            if (i + 1 == descriptions.length)
+            {
+                stackTrace = descriptions[i].getStackTrace();
+
+                for (j = 0; j < stackTrace.length; j++)
+                    stream.println(stackTrace[j]);
+            }
+            else
+                stream.println();
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/exception/ExceptionDescription.java b/tapestry-framework/src/org/apache/tapestry/util/exception/ExceptionDescription.java
new file mode 100644
index 0000000..d39f3b3
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/exception/ExceptionDescription.java
@@ -0,0 +1,76 @@
+//  Copyright 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.tapestry.util.exception;
+
+import java.io.Serializable;
+
+/**
+ *  A description of an <code>Exception</code>.  This is useful when presenting an
+ *  exception (in output or on a web page).
+ *
+ *  <p>We capture all the information about an exception as
+ *  Strings.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class ExceptionDescription implements Serializable
+{
+    /**
+     *  @since 2.0.4
+     * 
+     **/
+
+    private static final long serialVersionUID = -4874930784340781514L;
+
+    private String exceptionClassName;
+    private String message;
+    private ExceptionProperty[] properties;
+    private String[] stackTrace;
+
+    public ExceptionDescription(
+        String exceptionClassName,
+        String message,
+        ExceptionProperty[] properties,
+        String[] stackTrace)
+    {
+        this.exceptionClassName = exceptionClassName;
+        this.message = message;
+        this.properties = properties;
+        this.stackTrace = stackTrace;
+    }
+
+    public String getExceptionClassName()
+    {
+        return exceptionClassName;
+    }
+
+    public String getMessage()
+    {
+        return message;
+    }
+
+    public ExceptionProperty[] getProperties()
+    {
+        return properties;
+    }
+
+    public String[] getStackTrace()
+    {
+        return stackTrace;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/exception/ExceptionProperty.java b/tapestry-framework/src/org/apache/tapestry/util/exception/ExceptionProperty.java
new file mode 100644
index 0000000..7bcc484
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/exception/ExceptionProperty.java
@@ -0,0 +1,54 @@
+//  Copyright 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.tapestry.util.exception;
+
+import java.io.Serializable;
+
+/**
+ *  Captures a name/value property pair from an exception.  Part of
+ *  an {@link ExceptionDescription}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class ExceptionProperty implements Serializable
+{
+    /**
+     *  @since 2.0.4
+     * 
+     **/
+    
+    private static final long serialVersionUID = -4598312382467505134L;
+    private String name;
+    private String value;
+
+    public ExceptionProperty(String name, String value)
+    {
+        this.name = name;
+        this.value = value;
+    }
+
+    public String getName()
+    {
+        return name;
+    }
+
+    public String getValue()
+    {
+        return value;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/exception/package.html b/tapestry-framework/src/org/apache/tapestry/util/exception/package.html
new file mode 100644
index 0000000..d40c47b
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/exception/package.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+<p>A basic framework for analyzing a reporting exceptions.  The 
+{@link org.apache.tapestry.util.exception.ExceptionAnalyzer} class will identify
+the type, message and other properties of an exception, and understands about nested
+exceptions.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/util/io/BinaryDumpOutputStream.java b/tapestry-framework/src/org/apache/tapestry/util/io/BinaryDumpOutputStream.java
new file mode 100644
index 0000000..8fbd5a4
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/io/BinaryDumpOutputStream.java
@@ -0,0 +1,308 @@
+//  Copyright 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.tapestry.util.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.Writer;
+
+/**
+ *  A kind of super-formatter.  It is sent a stream of binary data and
+ *  formats it in a human-readable dump format which is forwarded to
+ *  its output stream.
+ *
+ * <p>Currently, output is in hex though options to change that may
+ * be introduced.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class BinaryDumpOutputStream extends OutputStream
+{
+    private PrintWriter out;
+
+    private boolean locked = false;
+
+    private boolean showOffset = true;
+    private int bytesPerLine = 16;
+    private int spacingInterval = 4;
+    private char substituteChar = '.';
+    private String offsetSeperator = ": ";
+    private int offset = 0;
+    private int lineCount = 0;
+    private int bytesSinceSpace = 0;
+    private char[] ascii = null;
+    private boolean showAscii = true;
+    private String asciiBegin = "  |";
+    private String asciiEnd = "|";
+
+    private static final char[] HEX =
+        {
+            '0',
+            '1',
+            '2',
+            '3',
+            '4',
+            '5',
+            '6',
+            '7',
+            '8',
+            '9',
+            'a',
+            'b',
+            'c',
+            'd',
+            'e',
+            'f' };
+
+    /**
+     *  Creates a <code>PrintWriter</code> for <code>System.out</code>.
+     *
+     **/
+
+    public BinaryDumpOutputStream()
+    {
+        this(new PrintWriter(System.out, true));
+    }
+
+    public BinaryDumpOutputStream(PrintWriter out)
+    {
+        this.out = out;
+    }
+
+    public BinaryDumpOutputStream(Writer out)
+    {
+        this.out = new PrintWriter(out);
+    }
+
+    public void close() throws IOException
+    {
+        if (out != null)
+        {
+            if (lineCount > 0)
+                finishFinalLine();
+
+            out.close();
+        }
+
+        out = null;
+    }
+
+    private void finishFinalLine()
+    {
+        // Since we only finish the final line after at least one byte has
+        // been written to it, we don't need to worry about
+        // the offset.
+
+        while (lineCount < bytesPerLine)
+        {
+            // After every <n> bytes, emit a space.
+
+            if (spacingInterval > 0 && bytesSinceSpace == spacingInterval)
+            {
+                out.print(' ');
+                bytesSinceSpace = 0;
+            }
+
+            // Two spaces to substitute for the two hex digits.
+
+            out.print("  ");
+
+            if (showAscii)
+                ascii[lineCount] = ' ';
+
+            lineCount++;
+            bytesSinceSpace++;
+        }
+
+        if (showAscii)
+        {
+            out.print(asciiBegin);
+            out.print(ascii);
+            out.print(asciiEnd);
+        }
+
+        out.println();
+    }
+
+    /**
+     *  Forwards the <code>flush()</code> to the <code>PrintWriter</code>.
+     *
+     **/
+
+    public void flush() throws IOException
+    {
+        out.flush();
+    }
+
+    public String getAsciiBegin()
+    {
+        return asciiBegin;
+    }
+
+    public String getAsciiEnd()
+    {
+        return asciiEnd;
+    }
+
+    public int getBytesPerLine()
+    {
+        return bytesPerLine;
+    }
+
+    public String getOffsetSeperator()
+    {
+        return offsetSeperator;
+    }
+
+    public boolean getShowAscii()
+    {
+        return showAscii;
+    }
+
+    public char getSubstituteChar()
+    {
+        return substituteChar;
+    }
+
+    public void setAsciiBegin(String value)
+    {
+        if (locked)
+            throw new IllegalStateException();
+
+        asciiBegin = value;
+    }
+
+    public void setAsciiEnd(String value)
+    {
+        if (locked)
+            throw new IllegalStateException();
+
+        asciiEnd = value;
+    }
+
+    public void setBytesPerLine(int value)
+    {
+        if (locked)
+            throw new IllegalStateException();
+
+        bytesPerLine = value;
+
+        ascii = null;
+    }
+
+    public void setOffsetSeperator(String value)
+    {
+        if (locked)
+            throw new IllegalStateException();
+
+        offsetSeperator = value;
+    }
+
+    public void setShowAscii(boolean value)
+    {
+        if (locked)
+            throw new IllegalStateException();
+
+        showAscii = value;
+    }
+
+    /**
+     *  Sets the character used in the ASCII dump that substitutes for characters
+     *  outside the range of 32..126.
+     *
+     **/
+
+    public void setSubstituteChar(char value)
+    {
+        if (locked)
+            throw new IllegalStateException();
+
+        substituteChar = value;
+    }
+
+    public void write(int b) throws IOException
+    {
+        char letter;
+
+        if (showAscii && ascii == null)
+            ascii = new char[bytesPerLine];
+
+        // Prevent further customization after output starts being written.
+
+        locked = true;
+
+        if (lineCount == bytesPerLine)
+        {
+            if (showAscii)
+            {
+                out.print(asciiBegin);
+                out.print(ascii);
+                out.print(asciiEnd);
+            }
+
+            out.println();
+
+            bytesSinceSpace = 0;
+            lineCount = 0;
+            offset += bytesPerLine;
+        }
+
+        if (lineCount == 0 && showOffset)
+        {
+            writeHex(offset, 4);
+            out.print(offsetSeperator);
+        }
+
+        // After every <n> bytes, emit a space.
+
+        if (spacingInterval > 0 && bytesSinceSpace == spacingInterval)
+        {
+            out.print(' ');
+            bytesSinceSpace = 0;
+        }
+
+        writeHex(b, 2);
+
+        if (showAscii)
+        {
+            if (b < 32 | b > 127)
+                letter = substituteChar;
+            else
+                letter = (char) b;
+
+            ascii[lineCount] = letter;
+        }
+
+        lineCount++;
+        bytesSinceSpace++;
+    }
+
+    private void writeHex(int value, int digits)
+    {
+        int i;
+        int nybble;
+
+        for (i = 0; i < digits; i++)
+        {
+            nybble = (value >> 4 * (digits - i - 1)) & 0x0f;
+
+            out.print(HEX[nybble]);
+        }
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/io/BooleanAdaptor.java b/tapestry-framework/src/org/apache/tapestry/util/io/BooleanAdaptor.java
new file mode 100644
index 0000000..ba397f1
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/io/BooleanAdaptor.java
@@ -0,0 +1,70 @@
+//  Copyright 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.tapestry.util.io;
+
+/**
+ *  Squeezes a {@link Boolean}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+class BooleanAdaptor implements ISqueezeAdaptor
+{
+    private static final String PREFIX = "TF";
+
+    /**
+     *  Registers using the prefixes 'T' and 'F' (for TRUE and FALSE).
+     *
+     **/
+
+    public void register(DataSqueezer squeezer)
+    {
+        squeezer.register(PREFIX, Boolean.class, this);
+    }
+
+    /**
+     *  Squeezes the {@link Boolean} data to either 'T' or 'F'.
+     *
+     **/
+
+    public String squeeze(DataSqueezer squeezer, Object data)
+    {
+        Boolean bool = (Boolean) data;
+
+        if (bool.booleanValue())
+            return "T";
+        else
+            return "F";
+    }
+
+    /**
+     *  Unsqueezes the string to either {@link Boolean#TRUE} or {@link Boolean#FALSE},
+     *  depending on the prefix character.
+     *
+     **/
+
+    public Object unsqueeze(DataSqueezer squeezer, String string)
+    {
+        char ch = string.charAt(0);
+
+        if (ch == 'T')
+            return Boolean.TRUE;
+
+        return Boolean.FALSE;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/io/ByteAdaptor.java b/tapestry-framework/src/org/apache/tapestry/util/io/ByteAdaptor.java
new file mode 100644
index 0000000..d6cf466
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/io/ByteAdaptor.java
@@ -0,0 +1,61 @@
+//  Copyright 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.tapestry.util.io;
+
+/**
+ *  Squeezes a {@link Byte}. 
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+class ByteAdaptor implements ISqueezeAdaptor
+{
+    private static final String PREFIX = "b";
+
+    /**
+     *  Registers using the prefix 'b'.
+     *
+     **/
+
+    public void register(DataSqueezer squeezer)
+    {
+        squeezer.register(PREFIX, Byte.class, this);
+    }
+
+    /**
+     *  Invoked <code>toString()</code> on data (which is type {@link Byte}),
+     *  and prefixs the result.
+     *
+     **/
+
+    public String squeeze(DataSqueezer squeezer, Object data)
+    {
+        return PREFIX + data.toString();
+    }
+
+    /**
+     *  Constructs an {@link Byte} from the string, after stripping
+     *  the prefix.
+     *
+     **/
+
+    public Object unsqueeze(DataSqueezer squeezer, String string)
+    {
+        return new Byte(string.substring(1));
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/io/CharacterAdaptor.java b/tapestry-framework/src/org/apache/tapestry/util/io/CharacterAdaptor.java
new file mode 100644
index 0000000..0703a0f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/io/CharacterAdaptor.java
@@ -0,0 +1,56 @@
+//  Copyright 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.tapestry.util.io;
+
+import java.io.IOException;
+
+/**
+ *  Squeezes a Character.
+ * 
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.2
+ * 
+ **/
+
+public class CharacterAdaptor implements ISqueezeAdaptor
+{
+    private static final String PREFIX = "c";
+    private static final char PREFIX_CH = 'c';
+    
+    public String squeeze(DataSqueezer squeezer, Object data) throws IOException
+    {
+        Character charData = (Character)data;
+        char value = charData.charValue();
+        
+        char[] buffer = new char[]
+        {
+            PREFIX_CH, value
+        };
+        
+        return new String(buffer);
+    }
+
+    public Object unsqueeze(DataSqueezer squeezer, String string) throws IOException
+    {
+        return new Character(string.charAt(1));
+    }
+
+    public void register(DataSqueezer squeezer)
+    {
+        squeezer.register(PREFIX, Character.class, this);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/io/ComponentAddressAdaptor.java b/tapestry-framework/src/org/apache/tapestry/util/io/ComponentAddressAdaptor.java
new file mode 100644
index 0000000..8b0ac52
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/io/ComponentAddressAdaptor.java
@@ -0,0 +1,67 @@
+//  Copyright 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.tapestry.util.io;
+
+import java.io.IOException;
+
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.util.ComponentAddress;
+
+/**
+ *  Squeezes a org.apache.tapestry.ComponentAddress.
+ * 
+ *  @author mindbridge
+ *  @version $Id$
+ *  @since 2.2
+ * 
+ **/
+
+public class ComponentAddressAdaptor implements ISqueezeAdaptor
+{
+    private static final String PREFIX = "A";
+    private static final char SEPARATOR = '/';
+    
+    public String squeeze(DataSqueezer squeezer, Object data) throws IOException
+    {
+        ComponentAddress address = (ComponentAddress) data;
+
+        // a 'null' id path is encoded as an empty string
+        String idPath = address.getIdPath();
+        if (idPath == null)
+        	idPath = "";
+
+        return PREFIX + address.getPageName() + SEPARATOR + idPath;
+    }
+
+    public Object unsqueeze(DataSqueezer squeezer, String string) throws IOException
+    {
+        int separator = string.indexOf(SEPARATOR);
+        if (separator < 0) 
+            throw new IOException(Tapestry.getMessage("ComponentAddressAdaptor.no-separator"));
+
+        String pageName = string.substring(1, separator);
+        String idPath = string.substring(separator + 1);
+        if (idPath.equals(""))
+        	idPath = null;
+
+        return new ComponentAddress(pageName, idPath);
+    }
+
+    public void register(DataSqueezer squeezer)
+    {
+        squeezer.register(PREFIX, ComponentAddress.class, this);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/io/DataSqueezer.java b/tapestry-framework/src/org/apache/tapestry/util/io/DataSqueezer.java
new file mode 100644
index 0000000..1bab14b
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/io/DataSqueezer.java
@@ -0,0 +1,299 @@
+//  Copyright 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.tapestry.util.io;
+
+import java.io.IOException;
+
+import org.apache.tapestry.IResourceResolver;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.util.AdaptorRegistry;
+
+/**
+ *  A class used to convert arbitrary objects to Strings and back.
+ *  This has particular uses involving HTTP URLs and Cookies.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+public class DataSqueezer
+{
+    private static final String NULL_PREFIX = "X";
+    private static final char NULL_PREFIX_CH = 'X';
+
+    private static final int ARRAY_SIZE = 90;
+    private static final int FIRST_ADAPTOR_OFFSET = 33;
+
+    /**
+     *  An array of adaptors; this is used as a cheap lookup-table when unsqueezing.  
+     *  Each adaptor is identified by a single ASCII character, in the range of
+     *  33 ('!') to 122 (the letter 'z').  The offset into this table
+     *  is the character minus 33.
+     *
+     **/
+
+    private ISqueezeAdaptor[] _adaptorByPrefix = new ISqueezeAdaptor[ARRAY_SIZE];
+
+    /**
+     *  AdaptorRegistry cache of adaptors.
+     *
+     **/
+
+    private AdaptorRegistry _adaptors = new AdaptorRegistry();
+
+    /**
+     *  Resource resolver used to deserialize classes.
+     * 
+     **/
+
+    private IResourceResolver _resolver;
+
+    /**
+     *  Creates a new squeezer with the default set of adaptors.
+     *
+     **/
+
+    public DataSqueezer(IResourceResolver resolver)
+    {
+        this(resolver, null);
+    }
+
+    /**
+     *  Creates a new data squeezer, which will have the default set of
+     *  adaptors, and may add additional adaptors.
+     *
+     *  @param adaptors an optional list of adaptors that will be registered to
+     *  the data squeezer (it may be null or empty)
+     *
+     **/
+
+    public DataSqueezer(IResourceResolver resolver, ISqueezeAdaptor[] adaptors)
+    {
+        _resolver = resolver;
+
+        registerDefaultAdaptors();
+
+        if (adaptors != null)
+            for (int i = 0; i < adaptors.length; i++)
+                adaptors[i].register(this);
+    }
+
+    private void registerDefaultAdaptors()
+    {
+        new CharacterAdaptor().register(this);
+        new StringAdaptor().register(this);
+        new IntegerAdaptor().register(this);
+        new DoubleAdaptor().register(this);
+        new ByteAdaptor().register(this);
+        new FloatAdaptor().register(this);
+        new LongAdaptor().register(this);
+        new ShortAdaptor().register(this);
+        new BooleanAdaptor().register(this);
+        new SerializableAdaptor().register(this);
+        new ComponentAddressAdaptor().register(this);
+        new EnumAdaptor().register(this);
+    }
+
+    /**
+     *  Registers the adaptor with one or more single-character prefixes.
+     *
+     *  @param prefix one or more characters, each of which will be a prefix for
+     *  the adaptor.
+     *  @param dataClass the class (or interface) which can be encoded by the adaptor.
+     *  @param adaptor the adaptor which to be registered.
+     *
+     **/
+
+    public synchronized void register(String prefix, Class dataClass, ISqueezeAdaptor adaptor)
+    {
+        int prefixLength = prefix.length();
+        int offset;
+
+        if (prefixLength < 1)
+            throw new IllegalArgumentException(Tapestry.getMessage("DataSqueezer.short-prefix"));
+
+        if (dataClass == null)
+            throw new IllegalArgumentException(Tapestry.getMessage("DataSqueezer.null-class"));
+
+        if (adaptor == null)
+            throw new IllegalArgumentException(Tapestry.getMessage("DataSqueezer.null-adaptor"));
+
+        for (int i = 0; i < prefixLength; i++)
+        {
+            char ch = prefix.charAt(i);
+
+            if (ch < '!' | ch > 'z')
+                throw new IllegalArgumentException(
+                    Tapestry.getMessage("DataSqueezer.prefix-out-of-range"));
+
+            offset = ch - FIRST_ADAPTOR_OFFSET;
+
+            if (_adaptorByPrefix[offset] != null)
+                throw new IllegalArgumentException(
+                    Tapestry.format(
+                        "DataSqueezer.adaptor-prefix-taken",
+                        prefix.substring(i, i)));
+
+            _adaptorByPrefix[offset] = adaptor;
+
+        }
+
+        _adaptors.register(dataClass, adaptor);
+    }
+
+    /**
+     *  Squeezes the data object into a String by locating an appropriate
+     *  adaptor that can perform the conversion.  data may be null.
+     *
+     **/
+
+    public String squeeze(Object data) throws IOException
+    {
+        ISqueezeAdaptor adaptor;
+
+        if (data == null)
+            return NULL_PREFIX;
+
+        adaptor = (ISqueezeAdaptor) _adaptors.getAdaptor(data.getClass());
+
+        return adaptor.squeeze(this, data);
+    }
+
+    /**
+     *  A convience; invokes {@link #squeeze(Object)} for each element in the
+     *  data array.  If data is null, returns null.
+     *
+     **/
+
+    public String[] squeeze(Object[] data) throws IOException
+    {
+        if (data == null)
+            return null;
+
+        int length = data.length;
+        String[] result;
+
+        result = new String[length];
+
+        for (int i = 0; i < length; i++)
+            result[i] = squeeze(data[i]);
+
+        return result;
+    }
+
+    /**
+     *  Unsqueezes the string.  Note that in a special case, where the first
+     *  character of the string is not a recognized prefix, it is assumed
+     *  that the string is simply a string, and return with no
+     *  change.
+     *
+     **/
+
+    public Object unsqueeze(String string) throws IOException
+    {
+        ISqueezeAdaptor adaptor = null;
+
+        if (string.equals(NULL_PREFIX))
+            return null;
+
+        int offset = string.charAt(0) - FIRST_ADAPTOR_OFFSET;
+
+        if (offset >= 0 && offset < _adaptorByPrefix.length)
+            adaptor = _adaptorByPrefix[offset];
+
+        // If the adaptor is not otherwise recognized, the it is simply
+        // an encoded String (the StringAdaptor may not have added
+        // a prefix).
+
+        if (adaptor == null)
+            return string;
+
+        // Adaptor should never be null, because we always supply
+        // an adaptor for String
+
+        return adaptor.unsqueeze(this, string);
+    }
+
+    /**
+     *  Convienience method for unsqueezing many strings (back into objects).
+     *  
+     *  <p>If strings is null, returns null.
+     * 
+     **/
+
+    public Object[] unsqueeze(String[] strings) throws IOException
+    {
+        if (strings == null)
+            return null;
+
+        int length = strings.length;
+        Object[] result;
+
+        result = new Object[length];
+
+        for (int i = 0; i < length; i++)
+            result[i] = unsqueeze(strings[i]);
+
+        return result;
+    }
+
+    /**
+     *  Checks to see if a given prefix character has a registered
+     *  adaptor.  This is used by the String adaptor to
+     *  determine whether it needs to put a prefix on its String.
+     *
+     **/
+
+    public boolean isPrefixRegistered(char prefix)
+    {
+        int offset = prefix - FIRST_ADAPTOR_OFFSET;
+
+        // Special case for handling nulls.
+
+        if (prefix == NULL_PREFIX_CH)
+            return true;
+
+        if (offset < 0 || offset >= _adaptorByPrefix.length)
+            return false;
+
+        return _adaptorByPrefix[offset] != null;
+    }
+
+    public String toString()
+    {
+        StringBuffer buffer;
+
+        buffer = new StringBuffer();
+        buffer.append("DataSqueezer[adaptors=<");
+        buffer.append(_adaptors.toString());
+        buffer.append(">]");
+
+        return buffer.toString();
+    }
+
+    /**
+     *  Returns the resource resolver used with this squeezer.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public IResourceResolver getResolver()
+    {
+        return _resolver;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/io/DoubleAdaptor.java b/tapestry-framework/src/org/apache/tapestry/util/io/DoubleAdaptor.java
new file mode 100644
index 0000000..fe55923
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/io/DoubleAdaptor.java
@@ -0,0 +1,61 @@
+//  Copyright 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.tapestry.util.io;
+
+/**
+ *  Squeezes a {@link Double}. 
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+class DoubleAdaptor implements ISqueezeAdaptor
+{
+    private static final String PREFIX = "d";
+
+    /**
+     *  Registers using the prefix 'd'.
+     *
+     **/
+
+    public void register(DataSqueezer squeezer)
+    {
+        squeezer.register(PREFIX, Double.class, this);
+    }
+
+    /**
+     *  Invoked <code>toString()</code> on data (which is type {@link Double}),
+     *  and prefixs the result.
+     *
+     **/
+
+    public String squeeze(DataSqueezer squeezer, Object data)
+    {
+        return PREFIX + data.toString();
+    }
+
+    /**
+     *  Constructs an {@link Double} from the string, after stripping
+     *  the prefix.
+     *
+     **/
+
+    public Object unsqueeze(DataSqueezer squeezer, String string)
+    {
+        return new Double(string.substring(1));
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/io/EnumAdaptor.java b/tapestry-framework/src/org/apache/tapestry/util/io/EnumAdaptor.java
new file mode 100644
index 0000000..63bbbeb
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/io/EnumAdaptor.java
@@ -0,0 +1,65 @@
+//  Copyright 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.tapestry.util.io;
+
+import java.io.IOException;
+
+import org.apache.commons.lang.enum.Enum;
+import org.apache.commons.lang.enum.EnumUtils;
+
+/**
+ *  Adaptor for {@link Enum} classes.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+public class EnumAdaptor implements ISqueezeAdaptor
+{
+    private static final String PREFIX = "E";
+    private static final char SEPARATOR = '@';
+
+    public String squeeze(DataSqueezer squeezer, Object o) throws IOException
+    {
+        Enum e = (Enum) o;
+        return PREFIX + e.getClass().getName() + SEPARATOR + e.getName();
+    }
+
+    public Object unsqueeze(DataSqueezer squeezer, String str) throws IOException
+    {
+        int pos = str.indexOf(SEPARATOR);
+
+        String className = str.substring(1, pos);
+        String name = str.substring(pos + 1, str.length());
+
+		Class enumClass = squeezer.getResolver().findClass(className);
+		
+        try
+        {        	
+            return EnumUtils.getEnum(enumClass, name);
+        }
+        catch (IllegalArgumentException ex)
+        {
+            throw new IOException(ex.getMessage());
+        }
+    }
+
+    public void register(DataSqueezer squeezer)
+    {
+        squeezer.register(PREFIX, Enum.class, this);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/io/FloatAdaptor.java b/tapestry-framework/src/org/apache/tapestry/util/io/FloatAdaptor.java
new file mode 100644
index 0000000..420e629
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/io/FloatAdaptor.java
@@ -0,0 +1,61 @@
+//  Copyright 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.tapestry.util.io;
+
+/**
+ *  Squeezes a {@link Float}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+class FloatAdaptor implements ISqueezeAdaptor
+{
+    private static final String PREFIX = "f";
+
+    /**
+     *  Registers using the prefix 'f'.
+     *
+     **/
+
+    public void register(DataSqueezer squeezer)
+    {
+        squeezer.register(PREFIX, Float.class, this);
+    }
+
+    /**
+     *  Invoked <code>toString()</code> on data (which is type {@link Float}),
+     *  and prefixs the result.
+     *
+     **/
+
+    public String squeeze(DataSqueezer squeezer, Object data)
+    {
+        return PREFIX + data.toString();
+    }
+
+    /**
+     *  Constructs a {@link Float} from the string, after stripping
+     *  the prefix.
+     *
+     **/
+
+    public Object unsqueeze(DataSqueezer squeezer, String string)
+    {
+        return new Float(string.substring(1));
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/io/ISqueezeAdaptor.java b/tapestry-framework/src/org/apache/tapestry/util/io/ISqueezeAdaptor.java
new file mode 100644
index 0000000..3ef6014
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/io/ISqueezeAdaptor.java
@@ -0,0 +1,55 @@
+//  Copyright 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.tapestry.util.io;
+
+import java.io.IOException;
+
+/**
+ *  Interface which defines a class used to convert data for a specific
+ *  Java type into a String format (squeeze it),
+ *  or convert from a String back into a Java type (unsqueeze).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public interface ISqueezeAdaptor
+{
+    /**
+     *  Converts the data object into a String.
+     *
+     *  @throws IOException if the object can't be converted.
+     **/
+
+    public String squeeze(DataSqueezer squeezer, Object data) throws IOException;
+
+    /**
+     *  Converts a String back into an appropriate object.
+     *
+     *  @throws IOException if the String can't be converted.
+     *
+     **/
+
+    public Object unsqueeze(DataSqueezer squeezer, String string)
+        throws IOException;
+
+    /**
+     *  Invoked to ask an adaptor to register itself to the squeezer.
+     *
+     **/
+
+    public void register(DataSqueezer squeezer);
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/io/IntegerAdaptor.java b/tapestry-framework/src/org/apache/tapestry/util/io/IntegerAdaptor.java
new file mode 100644
index 0000000..1b1fe82
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/io/IntegerAdaptor.java
@@ -0,0 +1,59 @@
+//  Copyright 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.tapestry.util.io;
+
+/**
+ *  Squeezes a {@link Integer}.  This adaptor claims all the digits as prefix
+ *  characters, so its the very simplest conversion of all!
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+class IntegerAdaptor implements ISqueezeAdaptor
+{
+    /**
+     *  Registers this adaptor using all nine digits and the minus sign.
+     *
+     **/
+
+    public void register(DataSqueezer squeezer)
+    {
+        squeezer.register("-0123456789", Integer.class, this);
+    }
+
+    /**
+     *  Simply invokes <code>toString()</code> on the data,
+     *  which is actually type {@link Integer}.
+     *
+     **/
+
+    public String squeeze(DataSqueezer squeezer, Object data)
+    {
+        return data.toString();
+    }
+
+    /**
+     *  Constructs an {@link Integer} from the string.
+     *
+     **/
+
+    public Object unsqueeze(DataSqueezer squeezer, String string)
+    {
+        return new Integer(string);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/io/LongAdaptor.java b/tapestry-framework/src/org/apache/tapestry/util/io/LongAdaptor.java
new file mode 100644
index 0000000..7e116a7
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/io/LongAdaptor.java
@@ -0,0 +1,61 @@
+//  Copyright 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.tapestry.util.io;
+
+/**
+ *  Squeezes a {@link Long}. 
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+class LongAdaptor implements ISqueezeAdaptor
+{
+    private static final String PREFIX = "l";
+
+    /**
+     *  Registers using the prefix 'l'.
+     *
+     **/
+
+    public void register(DataSqueezer squeezer)
+    {
+        squeezer.register(PREFIX, Long.class, this);
+    }
+
+    /**
+     *  Invoked <code>toString()</code> on data (which is type {@link Long}),
+     *  and prefixs the result.
+     *
+     **/
+
+    public String squeeze(DataSqueezer squeezer, Object data)
+    {
+        return PREFIX + data.toString();
+    }
+
+    /**
+     *  Constructs a {@link Long} from the string, after stripping
+     *  the prefix.
+     *
+     **/
+
+    public Object unsqueeze(DataSqueezer squeezer, String string)
+    {
+        return new Long(string.substring(1));
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/io/ResolvingObjectInputStream.java b/tapestry-framework/src/org/apache/tapestry/util/io/ResolvingObjectInputStream.java
new file mode 100644
index 0000000..4c8f20e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/io/ResolvingObjectInputStream.java
@@ -0,0 +1,57 @@
+//  Copyright 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.tapestry.util.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectStreamClass;
+
+import org.apache.tapestry.IResourceResolver;
+
+/**
+ *  Specialized subclass of {@link java.io.ObjectInputStream}
+ *  that knows how to resolve classes with a non-default
+ *  class loader (represented by an instance of
+ *  {@link org.apache.tapestry.IResourceResolver}).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class ResolvingObjectInputStream extends ObjectInputStream
+{
+    private IResourceResolver _resolver;
+
+    public ResolvingObjectInputStream(IResourceResolver resolver, InputStream input) throws IOException
+    {
+        super(input);
+
+        _resolver = resolver;
+    }
+
+    /**
+     *  Overrides the default implementation to
+     *  have the resource resolver find the class.
+     * 
+     **/
+
+    protected Class resolveClass(ObjectStreamClass v) throws IOException, ClassNotFoundException
+    {
+        return _resolver.findClass(v.getName());
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/io/SerializableAdaptor.java b/tapestry-framework/src/org/apache/tapestry/util/io/SerializableAdaptor.java
new file mode 100644
index 0000000..986596e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/io/SerializableAdaptor.java
@@ -0,0 +1,283 @@
+//  Copyright 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.tapestry.util.io;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  The most complicated of the adaptors, this one takes an arbitrary serializable
+ *  object, serializes it to binary, and encodes it in a Base64 encoding.
+ *
+ *  <p>Encoding and decoding of Base64 strings uses code adapted from work in the public
+ *  domain originally written by Jonathan Knudsen and published in
+ *  O'reilly's "Java Cryptography". Note that we use a <em>modified</em> form of Base64 encoding,
+ *  with URL-safe characters to encode the 62 and 63 values and the pad character.
+ *
+ *  <p>TBD:  Work out some class loader issues involved in deserializing.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+class SerializableAdaptor implements ISqueezeAdaptor
+{
+    private static final String PREFIX = "O";
+
+    /**
+     *  The PAD character, appended to the end of the string to make things
+     *  line up.  In normal Base64, this is the character '='.
+     *
+     **/
+
+    private static final char PAD = '.';
+
+    /**
+     *  Representation for the 6-bit code 63, normally '+' in Base64.
+     *
+     **/
+
+    private static final char CH_62 = '-';
+
+    /**
+     *  Representation for the 6-bit code 64, normally '/' in Base64.
+     *
+     **/
+
+    private static final char CH_63 = '_';
+
+    public String squeeze(DataSqueezer squeezer, Object data) throws IOException
+    {
+        ByteArrayOutputStream bos = null;
+        GZIPOutputStream gos = null;
+        ObjectOutputStream oos = null;
+        byte[] byteData = null;
+
+        try
+        {
+            bos = new ByteArrayOutputStream();
+            gos = new GZIPOutputStream(bos);
+            oos = new ObjectOutputStream(gos);
+
+            oos.writeObject(data);
+            oos.close();
+        }
+        finally
+        {
+            close(oos);
+            close(gos);
+            close(bos);
+        }
+
+        byteData = bos.toByteArray();
+
+        StringBuffer encoded = new StringBuffer(2 * byteData.length);
+        char[] base64 = new char[4];
+
+        encoded.append(PREFIX);
+
+        for (int i = 0; i < byteData.length; i += 3)
+        {
+            encodeBlock(byteData, i, base64);
+            encoded.append(base64);
+        }
+
+        return encoded.toString();
+    }
+
+    private void close(OutputStream stream)
+    {
+        if (stream != null)
+        {
+            try
+            {
+                stream.close();
+            }
+            catch (IOException ex)
+            {
+                // Ignore.
+            }
+        }
+    }
+
+    private void close(InputStream stream)
+    {
+        if (stream != null)
+        {
+            try
+            {
+                stream.close();
+            }
+            catch (IOException ex)
+            {
+                // Ignore.
+            }
+        }
+    }
+    public Object unsqueeze(DataSqueezer squeezer, String string) throws IOException
+    {
+        ByteArrayInputStream bis = null;
+        GZIPInputStream gis = null;
+        ObjectInputStream ois = null;
+        byte[] byteData;
+
+        // Strip off the first character and decode the rest.
+
+        byteData = decode(string.substring(1));
+
+        try
+        {
+            bis = new ByteArrayInputStream(byteData);
+            gis = new GZIPInputStream(bis);
+            ois = new ResolvingObjectInputStream(squeezer.getResolver(), gis);
+
+            return ois.readObject();
+        }
+        catch (ClassNotFoundException ex)
+        {
+            // The message is the name of the class.
+
+            throw new IOException(
+                Tapestry.format("SerializableAdaptor.class-not-found", ex.getMessage()));
+        }
+        finally
+        {
+            close(ois);
+            close(gis);
+            close(bis);
+        }
+    }
+
+    public void register(DataSqueezer squeezer)
+    {
+        squeezer.register(PREFIX, Serializable.class, this);
+    }
+
+    private static void encodeBlock(byte[] raw, int offset, char[] base64) throws IOException
+    {
+        int block = 0;
+        int slack = raw.length - offset - 1;
+        int end = (slack >= 2) ? 2 : slack;
+
+        for (int i = 0; i <= end; i++)
+        {
+            byte b = raw[offset + i];
+            int neuter = (b < 0) ? b + 256 : b;
+            block += neuter << (8 * (2 - i));
+        }
+
+        for (int i = 0; i < 4; i++)
+        {
+            int sixbit = (block >>> (6 * (3 - i))) & 0x3f;
+            base64[i] = getChar(sixbit);
+        }
+
+        if (slack < 1)
+            base64[2] = PAD;
+
+        if (slack < 2)
+            base64[3] = PAD;
+    }
+
+    protected static char getChar(int sixBit) throws IOException
+    {
+        if (sixBit >= 0 && sixBit <= 25)
+            return (char) ('A' + sixBit);
+
+        if (sixBit >= 26 && sixBit <= 51)
+            return (char) ('a' + (sixBit - 26));
+
+        if (sixBit >= 52 && sixBit <= 61)
+            return (char) ('0' + (sixBit - 52));
+
+        if (sixBit == 62)
+            return CH_62;
+
+        if (sixBit == 63)
+            return CH_63;
+
+        throw new IOException(
+            Tapestry.format("SerializableAdaptor.unable-to-convert", Integer.toString(sixBit)));
+    }
+
+    public static byte[] decode(String string) throws IOException
+    {
+        int pad = 0;
+        char[] base64 = string.toCharArray();
+
+        for (int i = base64.length - 1; base64[i] == PAD; i--)
+            pad++;
+
+        int length = base64.length * 6 / 8 - pad;
+        byte[] raw = new byte[length];
+        int rawIndex = 0;
+
+        for (int i = 0; i < base64.length; i += 4)
+        {
+            int block =
+                (getValue(base64[i]) << 18)
+                    + (getValue(base64[i + 1]) << 12)
+                    + (getValue(base64[i + 2]) << 6)
+                    + (getValue(base64[i + 3]));
+
+            for (int j = 0; j < 3 && rawIndex + j < raw.length; j++)
+                raw[rawIndex + j] = (byte) ((block >> (8 * (2 - j))) & 0xff);
+
+            rawIndex += 3;
+        }
+
+        return raw;
+    }
+
+    private static int getValue(char c) throws IOException
+    {
+        if (c >= 'A' && c <= 'Z')
+            return c - 'A';
+
+        if (c >= 'a' && c <= 'z')
+            return c - 'a' + 26;
+
+        if (c >= '0' && c <= '9')
+            return c - '0' + 52;
+
+        if (c == CH_62)
+            return 62;
+
+        if (c == CH_63)
+            return 63;
+
+        // Pad character
+
+        if (c == PAD)
+            return 0;
+
+        throw new IOException(
+            Tapestry.format(
+                "SerializableAdaptor.unable-to-interpret-char",
+                new String(new char[] { c })));
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/io/ShortAdaptor.java b/tapestry-framework/src/org/apache/tapestry/util/io/ShortAdaptor.java
new file mode 100644
index 0000000..8f7a1b4
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/io/ShortAdaptor.java
@@ -0,0 +1,61 @@
+//  Copyright 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.tapestry.util.io;
+
+/**
+ *  Squeezes a {@link Short}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+class ShortAdaptor implements ISqueezeAdaptor
+{
+    private static final String PREFIX = "s";
+
+    /**
+     *  Registers using the prefix 's'.
+     *
+     **/
+
+    public void register(DataSqueezer squeezer)
+    {
+        squeezer.register(PREFIX, Short.class, this);
+    }
+
+    /**
+     *  Invoked <code>toString()</code> on data (which is type {@link Short}),
+     *  and prefixs the result.
+     *
+     **/
+
+    public String squeeze(DataSqueezer squeezer, Object data)
+    {
+        return PREFIX + data.toString();
+    }
+
+    /**
+     *  Constructs a {@link Short} from the string, after stripping
+     *  the prefix.
+     *
+     **/
+
+    public Object unsqueeze(DataSqueezer squeezer, String string)
+    {
+        return new Short(string.substring(1));
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/io/StringAdaptor.java b/tapestry-framework/src/org/apache/tapestry/util/io/StringAdaptor.java
new file mode 100644
index 0000000..c7e6f03
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/io/StringAdaptor.java
@@ -0,0 +1,55 @@
+//  Copyright 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.tapestry.util.io;
+
+/**
+ *  Squeezes a String (which is pretty simple, most of the time).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+class StringAdaptor implements ISqueezeAdaptor
+{
+    private static final String PREFIX = "S";
+
+    public void register(DataSqueezer squeezer)
+    {
+        squeezer.register(PREFIX, String.class, this);
+    }
+
+    public String squeeze(DataSqueezer squeezer, Object data)
+    {
+        String string = (String) data;
+
+        return PREFIX + string;
+    }
+
+    /**
+     *  Strips the prefix from the string.  This method is only
+     *  invoked by the {@link DataSqueezer} if the string leads
+     *  with its normal prefix (an 'S').
+     *
+     **/
+
+    public Object unsqueeze(DataSqueezer squeezer, String string)
+    {
+        if (string.length() == 1)
+            return "";
+
+        return string.substring(1);
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/io/package.html b/tapestry-framework/src/org/apache/tapestry/util/io/package.html
new file mode 100644
index 0000000..2f4e8be
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/io/package.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+
+
+<body>
+
+<p>Some interesting I/O classes.  {@link org.apache.tapestry.util.io.BinaryDumpOutputStream}
+formats a stream of bytes into a human readable presentation, much like
+the Unix command line tool <code>od</code>.
+
+<p>{@link org.apache.tapestry.util.io.DataSqueezer} is used to squeeze and unsqueeze 
+basic scalar types, Strings and serializable objects into a String format.  The eventual
+purpose is to safely encode information into URLs or as HTTP Cookies.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/util/package.html b/tapestry-framework/src/org/apache/tapestry/util/package.html
new file mode 100644
index 0000000..a533dac
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/package.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+
+<body>
+
+<p>A general set of resuable classes and utilities for creating Internet and XML applications.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/util/pool/DefaultPoolableAdaptor.java b/tapestry-framework/src/org/apache/tapestry/util/pool/DefaultPoolableAdaptor.java
new file mode 100644
index 0000000..66ee7c4
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/pool/DefaultPoolableAdaptor.java
@@ -0,0 +1,44 @@
+//  Copyright 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.tapestry.util.pool;
+
+/**
+ *  Implementation for objects that implement
+ *  the {@link org.apache.tapestry.util.pool.IPoolable} interface.
+ * 
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class DefaultPoolableAdaptor implements IPoolableAdaptor
+{
+
+    public void resetForPool(Object object)
+    {
+        IPoolable poolable = (IPoolable)object;
+        
+        poolable.resetForPool();
+    }
+
+    public void discardFromPool(Object object)
+    {
+        IPoolable poolable = (IPoolable)object;
+        
+        poolable.discardFromPool();        
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/pool/IPoolable.java b/tapestry-framework/src/org/apache/tapestry/util/pool/IPoolable.java
new file mode 100644
index 0000000..cfd1da4
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/pool/IPoolable.java
@@ -0,0 +1,48 @@
+//  Copyright 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.tapestry.util.pool;
+
+/**
+ *  Marks an object as being aware that is to be stored into a {@link Pool}.
+ *  This gives the object a last chance to reset any state.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.4
+ * 
+ **/
+
+public interface IPoolable
+{
+    /**
+     *  Invoked by a {@link org.apache.tapestry.util.pool.Pool} 
+     *  just before the object is added to the pool.
+     *  The object should return its state to how it was when freshly instantiated
+     *  (or at least, its state should be indistinguishable from a freshly
+     *  instantiated instance).
+     *
+     **/
+
+    public void resetForPool();
+    
+    /**
+     *  Invoked just as a Pool discards an object (for lack of use).
+     *  This allows a last chance to perform final cleanup
+     *  on the object while it is still referencable.
+     * 
+     **/
+    
+    public void discardFromPool();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/pool/IPoolableAdaptor.java b/tapestry-framework/src/org/apache/tapestry/util/pool/IPoolableAdaptor.java
new file mode 100644
index 0000000..e93ee70
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/pool/IPoolableAdaptor.java
@@ -0,0 +1,44 @@
+//  Copyright 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.tapestry.util.pool;
+
+/**
+ *  Defines methods that define an adaptor to provide
+ *  {@link org.apache.tapestry.util.pool.IPoolable}
+ *  type behavior to arbitrary objects.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public interface IPoolableAdaptor
+{
+    /**
+     *  Invoked just as an object is returned to the pool; this
+     *  allows it to reset any state back to newly initialized.
+     * 
+     **/
+    
+    public void resetForPool(Object object);
+    
+    /**
+     *  Invoked when a pooled object is discarded from the pool.
+     * 
+     **/
+    
+    public void discardFromPool(Object object);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/pool/NullPoolableAdaptor.java b/tapestry-framework/src/org/apache/tapestry/util/pool/NullPoolableAdaptor.java
new file mode 100644
index 0000000..ae439e2
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/pool/NullPoolableAdaptor.java
@@ -0,0 +1,38 @@
+//  Copyright 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.tapestry.util.pool;
+
+/**
+ *  A default, empty implementation, for objects that
+ *  have no special behavior related to being pooled.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class NullPoolableAdaptor implements IPoolableAdaptor
+{
+
+    public void resetForPool(Object object)
+    {
+    }
+
+    public void discardFromPool(Object object)
+    {
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/pool/Pool.java b/tapestry-framework/src/org/apache/tapestry/util/pool/Pool.java
new file mode 100644
index 0000000..7d96ea2
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/pool/Pool.java
@@ -0,0 +1,516 @@
+//  Copyright 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.tapestry.util.pool;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.util.AdaptorRegistry;
+import org.apache.tapestry.util.ICleanable;
+import org.apache.tapestry.util.IRenderDescription;
+import org.apache.tapestry.util.JanitorThread;
+
+/**
+ *  A Pool is used to pool instances of a useful class.  It uses
+ *  keys, much like a {@link Map}, to identify a list of pooled objects.
+ *  Retrieving an object from the Pool atomically removes it from the
+ *  pool.  It can then be stored again later.  In this way, a single
+ *  Pool instance can manage many different types of pooled objects,
+ *  filed under different keys.
+ * 
+ *  <p>
+ *  Unlike traditional Pools, this class does not create new instances of
+ *  the objects it stores (with the exception of simple Java Beans,
+ *  via {@link #retrieve(Class)}.  The usage pattern is to retrieve an instance
+ *  from the Pool, and if the instance is null, create a new instance.
+ *
+ *  <p>The implementation of Pool is threadsafe.
+ *
+ *  <p>Pool implements {@link ICleanable}, with a goal of
+ *  only keeping pooled objects that have been needed within
+ *  a recent time frame.  A generational system is used, where each
+ *  pooled object is assigned a generation count.  {@link #executeCleanup}
+ *  discards objects whose generation count is too old (outside of a
+ *  {@link #getWindow() window}).
+ * 
+ *  <p>
+ *  Objects in the pool can receive two notifications: one notification
+ *  when they are {@link #store(Object, Object) stored} into the pool,
+ *  and one when they are discarded from the pool.
+ * 
+ *  <p>
+ *  Classes that implement {@link org.apache.tapestry.util.pool.IPoolable}
+ *  receive notifications directly, as per the two methods
+ *  of that interface.
+ * 
+ *  <p>
+ *  Alternately, an adaptor for the other classes can be
+ *  registerered (using {@link #registerAdaptor(Class, IPoolableAdaptor)}.
+ *  The adaptor will be invoked to handle the notification when a 
+ *  pooled object is stored or discarded.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+public class Pool implements ICleanable, IRenderDescription
+{
+    private static final Log LOG = LogFactory.getLog(Pool.class);
+
+    private AdaptorRegistry _adaptors = new AdaptorRegistry();
+
+    /**
+     *  The generation, used to cull unused pooled items.
+     *
+     *  @since 1.0.5
+     **/
+
+    private int _generation;
+
+    /**
+     *  The generation window, used to identify which
+     *  items should be culled.
+     *
+     *  @since 1.0.5
+     **/
+
+    private int _window = 10;
+
+    /**
+     *  The number of objects pooled.
+     *
+     **/
+
+    private int _pooledCount;
+
+    /**
+     *  A map of PoolLists, keyed on an arbitrary object.
+     *
+     **/
+
+    private Map _map;
+
+    /**
+     *  Creates a new Pool using the default map size.  Creation of the map is deferred.
+     *
+     *
+     **/
+
+    public Pool()
+    {
+        this(true);
+    }
+
+    /**
+     *  Creates a new Pool using the specified map size.  The map is created immediately.
+     *
+     *  @deprecated Use {@link #Pool()} instead.
+     * 
+     **/
+
+    public Pool(int mapSize)
+    {
+        this(mapSize, true);
+    }
+
+    /**
+     *  @param useSharedJanitor if true, then the Pool is added to
+     *  the {@link JanitorThread#getSharedJanitorThread() shared janitor}.
+     *
+     *  @since 1.0.5
+     *
+     **/
+
+    public Pool(boolean useSharedJanitor)
+    {
+        if (useSharedJanitor)
+            JanitorThread.getSharedJanitorThread().add(this);
+
+        registerAdaptors();
+    }
+
+    /**
+     *  Standard constructor.
+     *
+     *  @param mapSize initial size of the map.
+     *  @param useSharedJanitor if true, then the Pool is added to
+     *  the {@link JanitorThread#getSharedJanitorThread() shared janitor}.
+     *
+     *  @since 1.0.5
+     *  @deprecated Use {@link #Pool(boolean)} instead.
+     * 
+     **/
+
+    public Pool(int mapSize, boolean useSharedJanitor)
+    {
+        this(useSharedJanitor);
+
+        _map = new HashMap(mapSize);
+    }
+
+    /**
+     *  Returns the window used to cull pooled objects during a cleanup.
+     *  The default is 10, which works out to about five minutes with
+     *  a standard janitor (on a 30 second cycle).
+     *
+     *  @since 1.0.5
+     *
+     **/
+
+    public int getWindow()
+    {
+        return _window;
+    }
+
+    /**
+     *  Sets the window, or number of generations that an object may stay
+     *  in the pool before being culled.
+     *
+     *  @throws IllegalArgumentException if value is less than 1.
+     *
+     *  @since 1.0.5
+     **/
+
+    public void setWindow(int value)
+    {
+        if (value < 1)
+            throw new IllegalArgumentException("Pool window may not be less than 1.");
+
+        _window = value;
+    }
+
+    /**
+     *  Returns a previously pooled object with the given key, or null if no
+     *  such object exists.  Getting an object from a Pool removes it from the Pool,
+     *  but it can later be re-added with {@link #store(Object,Object)}.
+     *
+     **/
+
+    public synchronized Object retrieve(Object key)
+    {
+        Object result = null;
+
+        if (_map == null)
+            _map = new HashMap();
+
+        PoolList list = (PoolList) _map.get(key);
+
+        if (list != null)
+            result = list.retrieve();
+
+        if (result != null)
+            _pooledCount--;
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Retrieved " + result + " from " + key);
+
+        return result;
+    }
+
+    /**
+     *  Retrieves an instance of the named class.  If no pooled
+     *  instance is available, a new instance is created
+     *  (using the no arguments constructor).  Objects are
+     *  pooled using their actual class as a key.
+     * 
+     *  <p>
+     *  However, don't be fooled by false economies.  Unless
+     *  an object is very expensive to create, pooling is 
+     *  <em>more</em> expensive than simply instantiating temporary
+     *  instances and letting the garbage collector deal with it
+     *  (this is counter intuitive, but true).  For example,
+     *  this method was originally created to allow pooling
+     *  of {@link StringBuffer}, but testing showed that it
+     *  was a net defecit.
+     * 
+     **/
+
+    public Object retrieve(Class objectClass)
+    {
+        Object result = retrieve((Object) objectClass);
+
+        if (result == null)
+        {
+            if (LOG.isDebugEnabled())
+                LOG.debug("No instance of " + objectClass.getName() + " is available, instantiating one.");
+
+            try
+            {
+                result = objectClass.newInstance();
+            }
+            catch (Exception ex)
+            {
+                throw new ApplicationRuntimeException(
+                    Tapestry.format("Pool.unable-to-instantiate-instance", objectClass.getName()),
+                    ex);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     *  Stores an object using its class as a key.
+     * 
+     *  @see #retrieve(Class)
+     * 
+     **/
+
+    public void store(Object object)
+    {
+        store(object.getClass(), object);
+    }
+
+    /**
+     *  Stores an object in the pool for later retrieval, resetting
+     *  the object for storage within the pool.
+     *
+     **/
+
+    public synchronized void store(Object key, Object object)
+    {
+        getAdaptor(object).resetForPool(object);
+
+        if (_map == null)
+            _map = new HashMap();
+
+        PoolList list = (PoolList) _map.get(key);
+
+        if (list == null)
+        {
+            list = new PoolList(this);
+            _map.put(key, list);
+        }
+
+        int count = list.store(_generation, object);
+
+        _pooledCount++;
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Stored " + object + " into " + key + " (" + count + " pooled)");
+    }
+
+    /**
+     *  Removes all previously pooled objects from this Pool.
+     *
+     **/
+
+    public synchronized void clear()
+    {
+        if (_map != null)
+        {
+            Iterator i = _map.values().iterator();
+
+            while (i.hasNext())
+            {
+                PoolList list = (PoolList) i.next();
+
+                list.discardAll();
+            }
+
+            _map.clear();
+        }
+
+        _pooledCount = 0;
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Cleared");
+    }
+
+    /**
+     *  Returns the number of object pooled, the sum of the number
+     *  of objects in pooled under each key. 
+     *
+     *  @since 1.0.2
+     **/
+
+    public synchronized int getPooledCount()
+    {
+        return _pooledCount;
+    }
+
+    /**
+     *  Returns the number of keys within the pool.
+     *
+     *  @since 1.0.2
+     **/
+
+    public synchronized int getKeyCount()
+    {
+        if (_map == null)
+            return 0;
+
+        return _map.size();
+    }
+
+    /**
+     *  Peforms culling of unneeded pooled objects.
+     *
+     *  @since 1.0.5
+     *
+     **/
+
+    public synchronized void executeCleanup()
+    {
+        if (_map == null)
+            return;
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Executing cleanup of " + this);
+
+        _generation++;
+
+        int oldestGeneration = _generation - _window;
+
+        if (oldestGeneration < 0)
+            return;
+
+        int oldCount = _pooledCount;
+        int culledKeys = 0;
+
+        // During the cleanup, we keep the entire instance synchronized
+        // (meaning other threads will block when trying to store
+        // or retrieved pooled objects).  Fortunately, this
+        // should be pretty darn quick!
+
+        int newCount = 0;
+
+        Iterator i = _map.entrySet().iterator();
+        while (i.hasNext())
+        {
+            Map.Entry e = (Map.Entry) i.next();
+
+            PoolList list = (PoolList) e.getValue();
+
+            int count = list.cleanup(oldestGeneration);
+
+            if (count == 0)
+            {
+                i.remove();
+                culledKeys++;
+            }
+            else
+                newCount += count;
+        }
+
+        _pooledCount = newCount;
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Culled " + (oldCount - _pooledCount) + " pooled objects and " + culledKeys + " keys.");
+    }
+
+    public synchronized String toString()
+    {
+        ToStringBuilder builder = new ToStringBuilder(this);
+
+        builder.append("generation", _generation);
+        builder.append("pooledCount", _pooledCount);
+
+        return builder.toString();
+    }
+
+    /** @since 1.0.6 **/
+
+    public synchronized void renderDescription(IMarkupWriter writer)
+    {
+        writer.begin("table");
+        writer.attribute("border", "1");
+        writer.println();
+
+        writer.begin("tr");
+        writer.begin("th");
+        writer.attribute("colspan", "2");
+        writer.print(toString());
+        writer.end();
+        writer.end();
+        writer.println();
+
+        if (_map != null)
+        {
+            Iterator i = _map.entrySet().iterator();
+
+            while (i.hasNext())
+            {
+                Map.Entry entry = (Map.Entry) i.next();
+                PoolList list = (PoolList) entry.getValue();
+
+                writer.begin("tr");
+                writer.begin("td");
+                writer.print(entry.getKey().toString());
+                writer.end();
+                writer.begin("td");
+                writer.print(list.getPooledCount());
+                writer.end();
+                writer.end();
+
+                writer.println();
+            }
+        }
+    }
+
+    /**
+     *  Invoked from the constructor to register the default set of
+     *  {@link org.apache.tapestry.util.pool.IPoolableAdaptor}.  Subclasses
+     *  may override this to register a different set.
+     * 
+     *  <p>
+     *  Registers:
+     *  <ul>
+     *  <li>{@link NullPoolableAdaptor} for class Object
+     *  <li>{@link DefaultPoolableAdaptor} for interface {@link IPoolable}
+     *  <li>{@link StringBufferAdaptor} for {@link StringBuffer}
+     *  </ul>
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    protected void registerAdaptors()
+    {
+        registerAdaptor(Object.class, new NullPoolableAdaptor());
+        registerAdaptor(IPoolable.class, new DefaultPoolableAdaptor());
+        registerAdaptor(StringBuffer.class, new StringBufferAdaptor());
+    }
+
+    /**
+     *  Registers an adaptor for a particular class (or interface).
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    public void registerAdaptor(Class registrationClass, IPoolableAdaptor adaptor)
+    {
+        _adaptors.register(registrationClass, adaptor);
+    }
+
+    /**
+     *  Returns an adaptor appropriate to the object.
+     * 
+     **/
+
+    public IPoolableAdaptor getAdaptor(Object object)
+    {
+        return (IPoolableAdaptor) _adaptors.getAdaptor(object.getClass());
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/pool/PoolList.java b/tapestry-framework/src/org/apache/tapestry/util/pool/PoolList.java
new file mode 100644
index 0000000..26a50e3
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/pool/PoolList.java
@@ -0,0 +1,245 @@
+//  Copyright 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.tapestry.util.pool;
+
+/**
+ *  A wrapper around a list of objects for a given key in a {@link Pool}.
+ *  The current implementation of this is FIFO.  This class is closely
+ *  tied to {@link Pool}, which controls synchronization for it.
+ *
+ *  <p>This class, and {@link Pool}, were heavily revised in 1.0.5
+ *  to support generational cleaning.  The PoolList acts like a first-in
+ *  first-out queue and each pooled object is tagged with a "generation
+ *  count", provided by the {@link Pool}. The generation count is
+ *  incremented periodically.  This allows us to track, roughly,
+ *  how often a pooled object has been accessed; unused objects will
+ *  be buried with relatively low generation counts.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+class PoolList
+{
+    /** @since 3.0 **/
+
+    private Pool _pool;
+
+    /**
+     *  Linked list of pooled objects.
+     *
+     * @since 1.0.5
+     **/
+
+    private Entry _first;
+
+    /**
+     *  Linked list of "spare" Entries, ready to be re-used.
+     *
+     * @since 1.0.5
+     **/
+
+    private Entry _spare;
+
+    /**
+     *  Overall count of items pooled.
+     *
+     **/
+
+    private int _count;
+
+    /**
+     * A simple linked-list entry for items stored in the PoolList.
+     *
+     * @since 1.0.5
+     * 
+     **/
+
+    private static class Entry
+    {
+        int generation;
+        Object pooled;
+        Entry next;
+    }
+
+    /**
+     *  @since 3.0
+     * 
+     **/
+    
+    PoolList(Pool pool)
+    {
+        _pool = pool;
+    }
+
+    /**
+     *  Returns the number of pooled objects currently stored.
+     *
+     *  @since 1.0.5
+     **/
+
+    public int getPooledCount()
+    {
+        return _count;
+    }
+
+    /**
+     *  Returns an object previously stored into the list, or null if the list
+     *  is empty.  The returned object is removed from the list.
+     *
+     **/
+
+    public Object retrieve()
+    {
+        if (_count == 0)
+            return null;
+
+        _count--;
+
+        Entry e = _first;
+        Object result = e.pooled;
+
+        // Okay, store e into the list of spare entries.
+
+        _first = e.next;
+
+        e.next = _spare;
+        _spare = e;
+        e.generation = 0;
+        e.pooled = null;
+
+        return result;
+    }
+
+    /**
+     *  Adds the object to this PoolList.  An arbitrary number of objects can be
+     *  stored.  The objects can later be retrieved using {@link #get()}.
+     *  The list requires that generation never decrease.  On each subsequent
+     *  invocation, it should be the same as, or greater, than the previous value.
+     *
+     *  @return The number of objects stored in the list (after adding the new object).
+     **/
+
+    public int store(int generation, Object object)
+    {
+        Entry e;
+
+        if (_spare == null)
+        {
+            e = new Entry();
+        }
+        else
+        {
+            e = _spare;
+            _spare = _spare.next;
+        }
+
+        e.generation = generation;
+        e.pooled = object;
+        e.next = _first;
+        _first = e;
+
+        return ++_count;
+    }
+
+    /**
+     *  Invoked to cleanup the list, freeing unneeded objects.
+     *
+     * @param generation pooled objects stored in this generation or
+     * earlier are released.
+     *
+     * @since 1.0.5
+     **/
+
+    public int cleanup(int generation)
+    {
+        _spare = null;
+
+        _count = 0;
+
+        Entry prev = null;
+
+        // Walk through the list.  They'll be sorted by generation.
+
+        Entry e = _first;
+        while (true)
+        {
+            if (e == null)
+                break;
+
+            // If found a too-old entry then we want to
+            // delete it.
+
+            if (e.generation <= generation)
+            {
+                Object pooled = e.pooled;
+
+                // Notify the object that it is being dropped
+                // through the cracks!
+
+                _pool.getAdaptor(pooled).discardFromPool(pooled);
+
+                // Set the next pointer of the previous node to null.
+                // If the very first node inspected was too old,
+                // set the first pointer to null.
+
+                if (prev == null)
+                    _first = null;
+                else
+                    prev.next = null;
+            }
+            else
+                _count++;
+
+            prev = e;
+            e = e.next;
+        }
+
+        return _count;
+    }
+
+    public String toString()
+    {
+        return "PoolList[" + _count + "]";
+    }
+
+    /** 
+     *  Much like {@link #cleanup(int)}, but discards all
+     *  pooled objects.
+     * 
+     *  @since 3.0 
+     * 
+     **/
+
+    void discardAll()
+    {
+        Entry e = _first;
+
+        while (e != null)
+        {
+            Object pooled = e.pooled;
+
+            _pool.getAdaptor(pooled).discardFromPool(pooled);
+
+            e = e.next;
+        }
+
+        _first = null;
+        _spare = null;
+        _count = 0;
+
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/pool/StringBufferAdaptor.java b/tapestry-framework/src/org/apache/tapestry/util/pool/StringBufferAdaptor.java
new file mode 100644
index 0000000..61a8754
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/pool/StringBufferAdaptor.java
@@ -0,0 +1,43 @@
+//  Copyright 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.tapestry.util.pool;
+
+/**
+ *  Adaptor for {@link java.lang.StringBuffer}, that clears
+ *  the buffer as it is returned to the pool.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public class StringBufferAdaptor extends NullPoolableAdaptor
+{
+
+    /**
+     *  Sets the length of the {@link java.lang.StringBuffer}
+     *  to zero.
+     * 
+     **/
+    
+    public void resetForPool(Object object)
+    {
+        StringBuffer buffer = (StringBuffer)object;
+        
+        buffer.setLength(0);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/pool/package.html b/tapestry-framework/src/org/apache/tapestry/util/pool/package.html
new file mode 100644
index 0000000..1cde499
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/pool/package.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+
+<body>
+
+<p>Classes for managing a pool of reusable objects.  Rather than creating
+expensive objects as needed, they are obtained from a
+{@link org.apache.tapestry.util.pool.Pool}.  Instead of discarding them, they
+are returned to the Pool.   The Pool class is threadsafe and reasonably efficient.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/util/prop/OgnlUtils.java b/tapestry-framework/src/org/apache/tapestry/util/prop/OgnlUtils.java
new file mode 100644
index 0000000..c6852fc
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/prop/OgnlUtils.java
@@ -0,0 +1,161 @@
+//  Copyright 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.tapestry.util.prop;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import ognl.ClassResolver;
+import ognl.Ognl;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  Utilities wrappers around <a href="http://www.ognl.org">OGNL</a>.
+ * 
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.2
+ * 
+ **/
+
+public class OgnlUtils
+{
+    private static final Map _cache = new HashMap();
+
+    private OgnlUtils()
+    {
+    }
+
+    /**
+     *  Gets a parsed OGNL expression from the input string.
+     * 
+     *  @throws ApplicationRuntimeException if the expression can not be parsed.
+     * 
+     **/
+
+    public static synchronized Object getParsedExpression(String expression)
+    {
+        Object result = _cache.get(expression);
+
+        if (result == null)
+        {
+            try
+            {
+                result = Ognl.parseExpression(expression);
+            }
+            catch (Exception ex)
+            {
+                throw new ApplicationRuntimeException(
+                    Tapestry.format("OgnlUtils.unable-to-parse-expression", expression),
+                    ex);
+            }
+
+            _cache.put(expression, result);
+        }
+
+        return result;
+    }
+
+    /**
+     *  Parses and caches the expression and uses it to update
+     *  the target object with the provided value.
+     * 
+     *  @throws ApplicationRuntimeException if the expression
+     *  can not be parsed, or the target can not be updated.
+     * 
+     **/
+
+    public static void set(String expression, ClassResolver resolver, Object target, Object value)
+    {
+        set(getParsedExpression(expression), resolver, target, value);
+    }
+
+    /** 
+     *  Updates the target object with the provided value.
+     * 
+     *  @param expression a parsed OGNL expression
+     *  @throws ApplicationRuntimeException if the target can not be updated.
+     * 
+     **/
+
+    public static void set(Object expression, ClassResolver resolver, Object target, Object value)
+    {
+        try
+        {
+            Map context = Ognl.createDefaultContext(target, resolver);
+
+            Ognl.setValue(expression, context, target, value);
+        }
+        catch (Exception ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "OgnlUtils.unable-to-update-expression",
+                    "<parsed expression>",
+                    target,
+                    value),
+                ex);
+        }
+    }
+
+    /**
+     *   Returns the value of the expression evaluated against
+     *   the object.
+     * 
+     *   @param expression a parsed OGNL expression
+     *   @param object the root object
+     * 
+     *   @throws ApplicationRuntimeException
+     *   if the value can not be obtained from the object.
+     * 
+     **/
+
+    public static Object get(Object expression, ClassResolver resolver, Object object)
+    {
+        try
+        {
+            Map context = Ognl.createDefaultContext(object, resolver);
+
+            return Ognl.getValue(expression, context, object);
+        }
+        catch (Exception ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "OgnlUtils.unable-to-read-expression",
+                    "<parsed expression>",
+                    object),
+                ex);
+        }
+    }
+
+    /**
+     *   Returns the value of the expression evaluated against
+     *   the object.
+     * 
+     * 
+     *   @throws ApplicationRuntimeException if the
+     *   expression can not be parsed, or the value
+     *   not obtained from the object.
+     **/
+
+    public static Object get(String expression, ClassResolver resolver, Object object)
+    {
+        return get(getParsedExpression(expression), resolver, object);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/prop/PropertyFinder.java b/tapestry-framework/src/org/apache/tapestry/util/prop/PropertyFinder.java
new file mode 100644
index 0000000..75977c3
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/prop/PropertyFinder.java
@@ -0,0 +1,104 @@
+//  Copyright 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.tapestry.util.prop;
+
+import java.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  
+ *  Uses {@link java.beans.Introspector} to get bean information
+ *  and analyze properties for those beans.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.2
+ *
+ **/
+
+public class PropertyFinder
+{
+    /**
+     *  Keyed on bean class value is also a Map.  Inner Map is
+     *  keyed on property name, value is a
+     *  {@link PropertyInfo}.
+     * 
+     **/
+
+    private static Map _cache = new HashMap();
+
+    /**
+     *  Finds the {@link PropertyInfo} for the specified class and
+     *  property.  Returns null if the class does not implement 
+     *  such a property.
+     * 
+     **/
+    
+    public synchronized static PropertyInfo getPropertyInfo(Class beanClass, String propertyName)
+    {
+        Map beanClassMap = (Map) _cache.get(beanClass);
+
+        if (beanClassMap == null)
+        {
+            beanClassMap = buildBeanClassMap(beanClass);
+
+            _cache.put(beanClass, beanClassMap);
+        }
+
+        return (PropertyInfo) beanClassMap.get(propertyName);
+    }
+
+    private static Map buildBeanClassMap(Class beanClass)
+    {
+        Map result = new HashMap();
+        BeanInfo bi = null;
+
+        try
+        {
+            bi = Introspector.getBeanInfo(beanClass);
+        }
+        catch (IntrospectionException ex)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("PropertyFinder.unable-to-introspect-class", beanClass.getName()),
+                ex);
+        }
+
+        PropertyDescriptor[] pd = bi.getPropertyDescriptors();
+
+        for (int i = 0; i < pd.length; i++)
+        {
+            PropertyDescriptor d = pd[i];
+
+            PropertyInfo info =
+                new PropertyInfo(
+                    d.getName(),
+                    d.getPropertyType(),
+                    d.getReadMethod() != null,
+                    d.getWriteMethod() != null);
+
+            result.put(d.getName(), info);
+        }
+
+        return result;
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/prop/PropertyInfo.java b/tapestry-framework/src/org/apache/tapestry/util/prop/PropertyInfo.java
new file mode 100644
index 0000000..3fe2ad5
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/prop/PropertyInfo.java
@@ -0,0 +1,68 @@
+//  Copyright 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.tapestry.util.prop;
+
+/**
+ *  Used by {@link org.apache.tapestry.util.prop.PropertyFinder}
+ *  to identify information about a property. 
+ * 
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 2.2
+ *
+ **/
+
+public class PropertyInfo
+{
+    private String _name;
+    private Class _type;
+    private boolean _read;
+    private boolean _write;
+    
+    PropertyInfo(String name, Class type, boolean read, boolean write)
+    {
+        _name = name;
+        _type = type;
+        _read = read;
+        _write = write;
+    }
+    
+    public String getName()
+    {
+        return _name;
+    }
+
+    public Class getType()
+    {
+        return _type;
+    }
+
+    public boolean isRead()
+    {
+        return _read;
+    }
+
+    public boolean isWrite()
+    {
+        return _write;
+    }
+    
+    public boolean isReadWrite()
+    {
+        return _read && _write;
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/prop/package.html b/tapestry-framework/src/org/apache/tapestry/util/prop/package.html
new file mode 100644
index 0000000..1d864ac
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/prop/package.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+
+
+<body>
+
+<p>Classes for operating on Java Beans as collections of named properties.
+Prior to release 2.2, there was much more here, but with 2.2
+Tapestry switch to use the <a href="http://www.ognl.org">Object Graph
+Navigation Library</a> which is much more powerful.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/util/xml/BaseRule.java b/tapestry-framework/src/org/apache/tapestry/util/xml/BaseRule.java
new file mode 100644
index 0000000..e6d766a
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/xml/BaseRule.java
@@ -0,0 +1,63 @@
+//  Copyright 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.tapestry.util.xml;
+
+import org.apache.tapestry.Tapestry;
+import org.xml.sax.Attributes;
+
+/**
+ * Base implementation of {@link org.apache.tapestry.util.xml.IRule} that
+ * does nothing.
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * @since 3.0
+ **/
+public class BaseRule implements IRule
+{
+	protected String getAttribute(Attributes attributes, String name)
+	{
+		int count = attributes.getLength();
+
+		for (int i = 0; i < count; i++)
+		{
+			String attributeName = attributes.getLocalName(i);
+        	
+			if (Tapestry.isBlank(attributeName))
+				attributeName = attributes.getQName(i);
+        	
+			if (attributeName.equals(name))
+				return attributes.getValue(i);
+		}
+
+		return null;
+	}
+
+    public void startElement(RuleDirectedParser parser, Attributes attributes)
+    {
+
+    }
+
+    public void endElement(RuleDirectedParser parser)
+    {
+
+    }
+
+    public void content(RuleDirectedParser parser, String content)
+    {
+
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/xml/DocumentParseException.java b/tapestry-framework/src/org/apache/tapestry/util/xml/DocumentParseException.java
new file mode 100644
index 0000000..c56043f
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/xml/DocumentParseException.java
@@ -0,0 +1,105 @@
+//  Copyright 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.tapestry.util.xml;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.Location;
+import org.xml.sax.SAXParseException;
+
+/**
+ *  Exception thrown if there is any kind of error parsing the
+ *  an XML document. 
+ *
+ *  @see org.apache.tapestry.parse.SpecificationParser
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 0.2.10
+ *
+ **/
+
+public class DocumentParseException extends ApplicationRuntimeException
+{
+    private IResourceLocation _documentLocation;
+
+    public DocumentParseException(String message, Throwable rootCause)
+    {
+        this(message, null, null, rootCause);
+    }
+
+    public DocumentParseException(String message, IResourceLocation documentLocation)
+    {
+        this(message, documentLocation, null);
+    }
+
+    public DocumentParseException(
+        String message,
+        IResourceLocation documentLocation,
+        Throwable rootCause)
+    {
+        this(message, documentLocation, null, rootCause);
+    }
+
+    public DocumentParseException(
+        String message,
+        IResourceLocation documentLocation,
+        ILocation location,
+        Throwable rootCause)
+    {
+        super(message, null, location, rootCause);
+
+        _documentLocation = documentLocation;
+    }
+
+    public DocumentParseException(
+        String message,
+        IResourceLocation documentLocation,
+        SAXParseException rootCause)
+    {
+        this(
+            message,
+            documentLocation,
+            rootCause == null
+                || documentLocation == null
+                    ? null
+                    : new Location(
+                        documentLocation,
+                        rootCause.getLineNumber(),
+                        rootCause.getColumnNumber()),
+            rootCause);
+    }
+
+    public DocumentParseException(String message)
+    {
+        this(message, null, null, null);
+    }
+
+    public DocumentParseException(Throwable rootCause)
+    {
+        this(rootCause.getMessage(), rootCause);
+    }
+
+    public DocumentParseException(SAXParseException rootCause)
+    {
+        this(rootCause.getMessage(), (Throwable) rootCause);
+    }
+
+    public IResourceLocation getDocumentLocation()
+    {
+        return _documentLocation;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/util/xml/IRule.java b/tapestry-framework/src/org/apache/tapestry/util/xml/IRule.java
new file mode 100644
index 0000000..dd0ea44
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/xml/IRule.java
@@ -0,0 +1,56 @@
+//  Copyright 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.tapestry.util.xml;
+
+import org.xml.sax.Attributes;
+
+/**
+ * A rule that may be pushed onto the {@link org.apache.tapestry.util.xml.RuleDirectedParser}'s
+ * rule stack.  A rule is associated with an XML element.  It is pushed onto the stack when the
+ * open tag for the rule is encountered.  It is is popped off the stack after the end-tag is
+ * encountered.  It is notified about any text it directly wraps around.
+ * 
+ * <p>Rules should be stateless, because a rule instance may appear multiple times in the
+ * rule stack (if elements can be recusively nested).
+ *
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * @since 3.0
+ **/
+
+public interface IRule
+{
+	/**
+	 * Invoked just after the rule is pushed onto the rule stack.  Typically, a Rule will
+	 *  use the information to create a new object and push it onto the object stack.
+	 *  If the rule needs to know about the element (rather than the attributes), it
+	 *  may obtain the URI, localName and qName from the parser.
+	 * 
+	 */
+	public void startElement(RuleDirectedParser parser, Attributes attributes);
+	
+	/**
+	 * Invoked just after the rule is popped off the rule stack.
+	 */
+	public void endElement(RuleDirectedParser parser);
+	
+	
+	/**
+	 * Invoked when real content is found.  The parser is responsible for aggregating
+	 * all content provided by the underlying SAX parser into a single string.
+	 */
+	
+	public void content(RuleDirectedParser parser, String content);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/xml/InvalidStringException.java b/tapestry-framework/src/org/apache/tapestry/util/xml/InvalidStringException.java
new file mode 100644
index 0000000..429b793
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/xml/InvalidStringException.java
@@ -0,0 +1,56 @@
+//  Copyright 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.tapestry.util.xml;
+
+import org.apache.tapestry.ILocatable;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IResourceLocation;
+
+/**
+ *  Exception thrown if there is any kind of error validating a string
+ *  during document parsing
+ *
+ *  @author Geoffrey Longman
+ *  @version $Id$
+ *  @since 2.2
+ *
+ **/
+
+public class InvalidStringException extends DocumentParseException implements ILocatable
+{
+    private String _invalidString;
+
+    public InvalidStringException(
+        String message,
+        String invalidString,
+        IResourceLocation resourceLocation)
+    {
+        super(message, resourceLocation);
+
+        _invalidString = invalidString;
+    }
+
+    public InvalidStringException(String message, String invalidString, ILocation location)
+    {
+        super(message, location == null ? null : location.getResourceLocation(), location, null);
+
+        _invalidString = invalidString;
+    }
+
+    public String getInvalidString()
+    {
+        return _invalidString;
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/xml/RuleDirectedParser.java b/tapestry-framework/src/org/apache/tapestry/util/xml/RuleDirectedParser.java
new file mode 100644
index 0000000..1b79172
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/xml/RuleDirectedParser.java
@@ -0,0 +1,584 @@
+//  Copyright 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.tapestry.util.xml;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.ILocation;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.Location;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.util.RegexpMatcher;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * A simplified version of {@link org.apache.commons.digester.Digester}.
+ * This version is without as many bells and whistles but has some key features needed when parsing
+ * a document (rather than a configuration file):
+ * <br>
+ * <ul>
+ *   <li>Notifications for each bit of text</ul>
+ *   <li>Tracking of exact location within the document.</li>
+ * </ul>
+ * 
+ * <p>
+ * Like Digester, there's an object stack and a rule stack.  The rules are much
+ * simpler (more coding), in that there's a one-to-one relationship between
+ * an element and a rule.
+ *
+ * <p>
+ *  Based on SAX2.
+ * 
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * @since 3.0
+ */
+
+public class RuleDirectedParser extends DefaultHandler
+{
+    private static final Log LOG = LogFactory.getLog(RuleDirectedParser.class);
+
+    private IResourceLocation _documentLocation;
+    private List _ruleStack = new ArrayList();
+    private List _objectStack = new ArrayList();
+    private Object _documentObject;
+
+    private Locator _locator;
+    private int _line = -1;
+    private int _column = -1;
+    private ILocation _location;
+
+    private static SAXParserFactory _parserFactory;
+    private SAXParser _parser;
+
+    private RegexpMatcher _matcher;
+
+    private String _uri;
+    private String _localName;
+    private String _qName;
+
+    /**
+     *  Map of {@link IRule} keyed on the local name
+     *  of the element.
+     */
+    private Map _ruleMap = new HashMap();
+
+    /**
+     * Used to accumlate content provided by
+     * {@link org.xml.sax.ContentHandler#characters(char[], int, int)}.
+     */
+
+    private StringBuffer _contentBuffer = new StringBuffer();
+
+    /**
+     *  Map of paths to external entities (such as the DTD) keyed on public id.
+     */
+
+    private Map _entities = new HashMap();
+
+    public Object parse(IResourceLocation documentLocation)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Parsing: " + documentLocation);
+
+        try
+        {
+            _documentLocation = documentLocation;
+
+            URL url = documentLocation.getResourceURL();
+
+            if (url == null)
+                throw new DocumentParseException(
+                    Tapestry.format("RuleDrivenParser.resource-missing", documentLocation),
+                    documentLocation,
+                    null,
+                    null);
+
+            return parse(url);
+        }
+        finally
+        {
+            _documentLocation = null;
+            _ruleStack.clear();
+            _objectStack.clear();
+            _documentObject = null;
+
+            _uri = null;
+            _localName = null;
+            _qName = null;
+
+            _line = -1;
+            _column = -1;
+            _location = null;
+            _locator = null;
+
+            _contentBuffer.setLength(0);
+        }
+    }
+
+    protected Object parse(URL url)
+    {
+        if (_parser == null)
+            _parser = constructParser();
+
+        InputStream stream = null;
+
+        try
+        {
+            stream = url.openStream();
+        }
+        catch (IOException ex)
+        {
+            throw new DocumentParseException(
+                Tapestry.format("RuleDrivenParser.unable-to-open-resource", url),
+                _documentLocation,
+                null,
+                ex);
+        }
+
+        InputSource source = new InputSource(stream);
+
+        try
+        {
+            _parser.parse(source, this);
+
+            stream.close();
+        }
+        catch (Exception ex)
+        {
+            throw new DocumentParseException(
+                Tapestry.format("RuleDrivenParser.parse-error", url, ex.getMessage()),
+                _documentLocation,
+                getLocation(),
+                ex);
+        }
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Document parsed as: " + _documentObject);
+
+        return _documentObject;
+    }
+
+    /**
+     * Returns an {@link ILocation} representing the
+     * current position within the document (depending
+     * on the parser, this may be accurate to
+     * column number level).
+     */
+
+    public ILocation getLocation()
+    {
+        if (_locator == null)
+            return null;
+
+        int line = _locator.getLineNumber();
+        int column = _locator.getColumnNumber();
+
+        if (_line != line || _column != column)
+        {
+            _location = null;
+            _line = line;
+            _column = column;
+        }
+
+        if (_location == null)
+            _location = new Location(_documentLocation, _line, _column);
+
+        return _location;
+    }
+
+    /**
+     * Pushes an object onto the object stack.  The first object
+     * pushed is the "document object", the root object returned
+     * by the parse.
+     */
+    public void push(Object object)
+    {
+        if (_documentObject == null)
+            _documentObject = object;
+
+        push(_objectStack, object, "object stack");
+    }
+
+    /**
+     * Returns the top object on the object stack.
+     */
+    public Object peek()
+    {
+        return peek(_objectStack, 0);
+    }
+
+    /**
+     * Returns an object within the object stack, at depth.
+     * Depth 0 is the top object, depth 1 is the next-to-top object,
+     * etc.
+     */
+
+    public Object peek(int depth)
+    {
+        return peek(_objectStack, depth);
+    }
+
+    /**
+     * Removes and returns the top object on the object stack.
+     */
+    public Object pop()
+    {
+        return pop(_objectStack, "object stack");
+    }
+
+    private Object pop(List list, String name)
+    {
+        Object result = list.remove(list.size() - 1);
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("Popped " + result + " off " + name + " (at " + getLocation() + ")");
+
+        return result;
+    }
+
+    private Object peek(List list, int depth)
+    {
+        return list.get(list.size() - 1 - depth);
+    }
+
+    private void push(List list, Object object, String name)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Pushing " + object + " onto " + name + " (at " + getLocation() + ")");
+
+        list.add(object);
+    }
+
+    /**
+     * Pushes a new rule onto the rule stack.
+     */
+
+    protected void pushRule(IRule rule)
+    {
+        push(_ruleStack, rule, "rule stack");
+    }
+
+    /**
+     * Returns the top rule on the stack.
+     */
+
+    protected IRule peekRule()
+    {
+        return (IRule) peek(_ruleStack, 0);
+    }
+
+    protected IRule popRule()
+    {
+        return (IRule) pop(_ruleStack, "rule stack");
+    }
+
+    public void addRule(String localElementName, IRule rule)
+    {
+        _ruleMap.put(localElementName, rule);
+    }
+
+    /**
+     *  Registers
+     *  a public id and corresponding input source.  Generally, the source
+     *  is a wrapper around an input stream to a package resource.
+     *
+     *  @param publicId the public identifier to be registerred, generally
+     *  the publicId of a DTD related to the document being parsed
+     *  @param entityPath the resource path of the entity, typically a DTD
+     *  file.  Relative files names are expected to be stored in the same package
+     *  as the class file, otherwise a leading slash is an absolute pathname
+     *  within the classpath.
+     *
+     **/
+
+    public void registerEntity(String publicId, String entityPath)
+    {
+        if (LOG.isDebugEnabled())
+            LOG.debug("Registering " + publicId + " as " + entityPath);
+
+        if (_entities == null)
+            _entities = new HashMap();
+
+        _entities.put(publicId, entityPath);
+    }
+
+    protected IRule selectRule(String localName, Attributes attributes)
+    {
+        IRule rule = (IRule) _ruleMap.get(localName);
+
+        if (rule == null)
+            throw new DocumentParseException(
+                Tapestry.format("RuleDrivenParser.no-rule-for-element", localName),
+                _documentLocation,
+                getLocation(),
+                null);
+
+        return rule;
+    }
+
+    /**
+     * Uses the {@link Locator} to track the position
+     * in the document as a {@link ILocation}. This is invoked
+     * once (before the initial element is parsed) and
+     * the Locator is retained and queried as to
+     * the current file location.
+     * 
+     * @see #getLocation()
+     */
+    public void setDocumentLocator(Locator locator)
+    {
+        _locator = locator;
+    }
+
+    /**
+     * Accumulates the content in a buffer; the concatinated content
+     * is provided to the top rule just before any start or end tag. 
+     */
+    public void characters(char[] ch, int start, int length) throws SAXException
+    {
+        _contentBuffer.append(ch, start, length);
+    }
+
+    /**
+     * Pops the top rule off the stack and
+     * invokes {@link IRule#endElement(RuleDirectedParser)}.
+     */
+    public void endElement(String uri, String localName, String qName) throws SAXException
+    {
+        fireContentRule();
+
+        _uri = uri;
+        _localName = localName;
+        _qName = qName;
+
+        popRule().endElement(this);
+    }
+
+    /**
+     * Ignorable content is ignored.
+     */
+    public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException
+    {
+    }
+
+    /**
+     * Invokes {@link #selectRule(String, Attributes)} to choose a new rule,
+     * which is pushed onto the rule stack, then invokes
+     * {@link IRule#startElement(RuleDirectedParser, Attributes)}.
+     */
+    public void startElement(String uri, String localName, String qName, Attributes attributes)
+        throws SAXException
+    {
+        fireContentRule();
+
+        _uri = uri;
+        _localName = localName;
+        _qName = qName;
+
+        String name = extractName(uri, localName, qName);
+
+        IRule newRule = selectRule(name, attributes);
+
+        pushRule(newRule);
+
+        newRule.startElement(this, attributes);
+    }
+
+    private String extractName(String uri, String localName, String qName)
+    {
+        return Tapestry.isBlank(localName) ? qName : localName;
+    }
+
+    /**
+     * Uses {@link javax.xml.parsers.SAXParserFactory} to create a instance
+     * of a validation SAX2 parser.
+     */
+    protected synchronized SAXParser constructParser()
+    {
+        if (_parserFactory == null)
+        {
+            _parserFactory = SAXParserFactory.newInstance();
+            configureParserFactory(_parserFactory);
+        }
+
+        try
+        {
+            return _parserFactory.newSAXParser();
+        }
+        catch (SAXException ex)
+        {
+            throw new ApplicationRuntimeException(ex);
+        }
+        catch (ParserConfigurationException ex)
+        {
+            throw new ApplicationRuntimeException(ex);
+        }
+
+    }
+
+    /**
+     * Configures a {@link SAXParserFactory} before
+     * {@link SAXParserFactory#newSAXParser()} is invoked.
+     * The default implementation sets validating to true
+     * and namespaceAware to false,
+     */
+
+    protected void configureParserFactory(SAXParserFactory factory)
+    {
+        factory.setValidating(true);
+        factory.setNamespaceAware(false);
+    }
+
+    /**
+     *  Throws the exception.
+     */
+    public void error(SAXParseException ex) throws SAXException
+    {
+        fatalError(ex);
+    }
+
+    /**
+     *  Throws the exception.
+     */
+    public void fatalError(SAXParseException ex) throws SAXException
+    {
+        // Sometimes, a bad parse "corrupts" a parser so that it doesn't
+        // work properly for future parses (of valid documents),
+        // so discard it here.
+
+        _parser = null;
+
+        throw ex;
+    }
+
+    /**
+     *  Throws the exception.
+     */
+    public void warning(SAXParseException ex) throws SAXException
+    {
+        fatalError(ex);
+    }
+
+    public InputSource resolveEntity(String publicId, String systemId) throws SAXException
+    {
+        String entityPath = null;
+
+        if (LOG.isDebugEnabled())
+            LOG.debug(
+                "Attempting to resolve entity; publicId = " + publicId + " systemId = " + systemId);
+
+        if (_entities != null)
+            entityPath = (String) _entities.get(publicId);
+
+        if (entityPath == null)
+        {
+            if (LOG.isDebugEnabled())
+                LOG.debug("Entity not found, using " + systemId);
+
+            return null;
+        }
+
+        InputStream stream = getClass().getResourceAsStream(entityPath);
+
+        InputSource result = new InputSource(stream);
+
+        if (result != null && LOG.isDebugEnabled())
+            LOG.debug("Resolved " + publicId + " as " + result + " (for " + entityPath + ")");
+
+        return result;
+    }
+
+    /**
+     *  Validates that the input value matches against the specified
+     *  Perl5 pattern.  If valid, the method simply returns.
+     *  If not a match, then an error message is generated (using the
+     *  errorKey and the input value) and a
+     *  {@link InvalidStringException} is thrown.
+     * 
+     **/
+
+    public void validate(String value, String pattern, String errorKey)
+        throws DocumentParseException
+    {
+        if (_matcher == null)
+            _matcher = new RegexpMatcher();
+
+        if (_matcher.matches(pattern, value))
+            return;
+
+        throw new InvalidStringException(Tapestry.format(errorKey, value), value, getLocation());
+    }
+
+    public IResourceLocation getDocumentLocation()
+    {
+        return _documentLocation;
+    }
+
+    /**
+     * Returns the localName for the current element.
+     * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
+     */
+    public String getLocalName()
+    {
+        return _localName;
+    }
+
+    /**
+     * Returns the qualified name for the current element.
+     * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
+     */
+    public String getQName()
+    {
+        return _qName;
+    }
+
+    /**
+     * Returns the URI for the current element.
+     * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes) 
+     */
+    public String getUri()
+    {
+        return _uri;
+    }
+
+    private void fireContentRule()
+    {
+        String content = _contentBuffer.toString();
+        _contentBuffer.setLength(0);
+
+        if (!_ruleStack.isEmpty())
+            peekRule().content(this, content);
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/util/xml/package.html b/tapestry-framework/src/org/apache/tapestry/util/xml/package.html
new file mode 100644
index 0000000..16de827
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/util/xml/package.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+
+<body>
+
+<p>Base classes for streamlining the process of parsing an XML document.  This is primarily 
+used with a validating parser, where the DTD is stored within the classpath.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/BaseValidator.java b/tapestry-framework/src/org/apache/tapestry/valid/BaseValidator.java
new file mode 100644
index 0000000..b8f6bbf
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/BaseValidator.java
@@ -0,0 +1,350 @@
+//  Copyright 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.tapestry.valid;
+
+import java.text.MessageFormat;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.ResourceBundle;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IEngine;
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.IResourceLocation;
+import org.apache.tapestry.IScript;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.engine.IScriptSource;
+import org.apache.tapestry.form.FormEventType;
+import org.apache.tapestry.form.IFormComponent;
+import org.apache.tapestry.html.Body;
+import org.apache.tapestry.resource.ClasspathResourceLocation;
+
+/**
+ *  Abstract base class for {@link IValidator}.  Supports a required and locale property.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.8
+ *
+ **/
+
+public abstract class BaseValidator implements IValidator
+{
+    /**
+     *  Input Symbol used to represent the field being validated.
+     * 
+     *  @see #processValidatorScript(String, IRequestCycle, IFormComponent, Map)
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public static final String FIELD_SYMBOL = "field";
+
+    /**
+     *  Input symbol used to represent the validator itself to the script.
+     * 
+     *  @see #processValidatorScript(String, IRequestCycle, IFormComponent, Map)
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public static final String VALIDATOR_SYMBOL = "validator";
+
+    /**
+     *  Input symbol used to represent the {@link IForm} containing the field
+     *  to the script.
+     * 
+     *  @see #processValidatorScript(String, IRequestCycle, IFormComponent, Map)
+     *  
+     *  @since 2.2
+     **/
+
+    public static final String FORM_SYMBOL = "form";
+
+    /**
+     *  Output symbol set by the script asthe name of the validator 
+     *  JavaScript function.
+     *  The function implemented must return true or false (true
+     *  if the field is valid, false otherwise).
+     *  After the script is executed, the function is added
+     *  to the {@link IForm} as a {@link org.apache.tapestry.form.FormEventType#SUBMIT}.
+     * 
+     *  @see #processValidatorScript(String, IRequestCycle, IFormComponent, Map)
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public static final String FUNCTION_SYMBOL = "function";
+
+    private boolean _required;
+
+    /** @since 3.0 */
+
+    private String _requiredMessage;
+
+    /** 
+     *  @since 2.2
+     * 
+     **/
+
+    private boolean _clientScriptingEnabled = false;
+
+    /**
+     *  Standard constructor.  Leaves locale as system default and required as false.
+     * 
+     **/
+
+    public BaseValidator()
+    {
+    }
+
+    protected BaseValidator(boolean required)
+    {
+        _required = required;
+    }
+
+    public boolean isRequired()
+    {
+        return _required;
+    }
+
+    public void setRequired(boolean required)
+    {
+        _required = required;
+    }
+
+    /**
+     * Gets a pattern, either as the default value, or as a localized key.
+     * If override is null, then the key from the
+     * <code>org.apache.tapestry.valid.ValidationStrings</code>
+     * {@link ResourceBundle} (in the specified locale) is used.
+     * The pattern can then be used with {@link #formatString(String, Object[])}.
+     * 
+     * <p>Why do we not just lump these strings into TapestryStrings.properties?  
+     * because TapestryStrings.properties is localized to the server's locale, which is fine
+     * for the logging, debugging and error messages it contains.  For field validation, whose errors
+     * are visible to the end user normally, we want to localize to the page's locale.
+     *  
+     * @param override The override value for the localized string from the bundle.
+     * @param key used to lookup pattern from bundle, if override is null.
+     * @param locale used to get right localization of bundle.
+     * @since 3.0
+     */
+
+    protected String getPattern(String override, String key, Locale locale)
+    {
+        if (override != null)
+            return override;
+
+        ResourceBundle strings =
+            ResourceBundle.getBundle("org.apache.tapestry.valid.ValidationStrings", locale);
+
+        return strings.getString(key);
+    }
+
+    /**
+     * Gets a string from the standard resource bundle.  The string in the bundle
+     * is treated as a pattern for {@link MessageFormat#format(java.lang.String, java.lang.Object[])}.
+     * 
+     * @param pattern string the input pattern to be used with
+     * {@link MessageFormat#format(java.lang.String, java.lang.Object[])}.
+     * It may contain replaceable parameters, {0}, {1}, etc.
+     * @param args the arguments used to fill replaceable parameters {0}, {1}, etc.
+     * 
+     * @since 3.0
+     * 
+     **/
+
+    protected String formatString(String pattern, Object[] args)
+    {
+        return MessageFormat.format(pattern, args);
+    }
+
+    /**
+     *  Convienience method for invoking {@link #formatString(String, Object[])}.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    protected String formatString(String pattern, Object arg)
+    {
+        return formatString(pattern, new Object[] { arg });
+    }
+
+    /**
+     *  Convienience method for invoking {@link #formatString(String, Object[])}.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    protected String formatString(String pattern, Object arg1, Object arg2)
+    {
+        return formatString(pattern, new Object[] { arg1, arg2 });
+    }
+
+    /**
+     *  Invoked to check if the value is null.  If the value is null (or empty),
+     *  but the required flag is set, then this method throws a {@link ValidatorException}.
+     *  Otherwise, returns true if the value is null.
+     * 
+     **/
+
+    protected boolean checkRequired(IFormComponent field, String value) throws ValidatorException
+    {
+        boolean isEmpty = Tapestry.isBlank(value);
+
+        if (_required && isEmpty)
+            throw new ValidatorException(
+                buildRequiredMessage(field),
+                ValidationConstraint.REQUIRED);
+
+        return isEmpty;
+    }
+
+    /** 
+     * Builds an error message indicating a value for a required
+     * field was not supplied.
+     * 
+     * @since 3.0
+     */
+
+    protected String buildRequiredMessage(IFormComponent field)
+    {
+        String pattern =
+            getPattern(_requiredMessage, "field-is-required", field.getPage().getLocale());
+
+        return formatString(pattern, field.getDisplayName());
+    }
+
+    /**
+     *  This implementation does nothing.  Subclasses may supply their own implementation.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public void renderValidatorContribution(
+        IFormComponent field,
+        IMarkupWriter writer,
+        IRequestCycle cycle)
+    {
+    }
+
+    /**
+     *  Invoked (from sub-class
+     *  implementations of {@link #renderValidatorContribution(IFormComponent, IMarkupWriter, IRequestCycle)}
+     *  to process a standard validation script.  This expects that:
+     *  <ul>
+     *  <li>The {@link IFormComponent} is (ultimately) wrapped by a {@link Body}
+     *  <li>The script generates a symbol named "function" (as per {@link #FUNCTION_SYMBOL})
+     *  </ul>
+     * 
+     *  @param scriptPath the resource path of the script to execute
+     *  @param cycle The active request cycle
+     *  @param field The field to be validated
+     *  @param symbols a set of input symbols needed by the script.  These symbols
+     *  are augmented with symbols for the field, form and validator.  symbols may be
+     *  null, but will be modified if not null.
+     *  @throws ApplicationRuntimeException if there's an error processing the script.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    protected void processValidatorScript(
+        String scriptPath,
+        IRequestCycle cycle,
+        IFormComponent field,
+        Map symbols)
+    {
+        IEngine engine = field.getPage().getEngine();
+        IScriptSource source = engine.getScriptSource();
+        IForm form = field.getForm();
+
+        Map finalSymbols = (symbols == null) ? new HashMap() : symbols;
+
+        finalSymbols.put(FIELD_SYMBOL, field);
+        finalSymbols.put(FORM_SYMBOL, form);
+        finalSymbols.put(VALIDATOR_SYMBOL, this);
+
+        IResourceLocation location =
+            new ClasspathResourceLocation(engine.getResourceResolver(), scriptPath);
+
+        IScript script = source.getScript(location);
+
+        Body body = Body.get(cycle);
+
+        if (body == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("ValidField.must-be-contained-by-body"),
+                field,
+                null,
+                null);
+
+        script.execute(cycle, body, finalSymbols);
+
+        String functionName = (String) finalSymbols.get(FUNCTION_SYMBOL);
+
+        form.addEventHandler(FormEventType.SUBMIT, functionName);
+    }
+
+    /**
+     *  Returns true if client scripting is enabled.  Some validators are
+     *  capable of generating client-side scripting to perform validation
+     *  when the form is submitted.  By default, this flag is false and
+     *  subclasses should check it 
+     *  (in {@link #renderValidatorContribution(IFormComponent, IMarkupWriter, IRequestCycle)})
+     *  before generating client side script.
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public boolean isClientScriptingEnabled()
+    {
+        return _clientScriptingEnabled;
+    }
+
+    public void setClientScriptingEnabled(boolean clientScriptingEnabled)
+    {
+        _clientScriptingEnabled = clientScriptingEnabled;
+    }
+
+    public String getRequiredMessage()
+    {
+        return _requiredMessage;
+    }
+
+    /**
+     * Overrides the <code>field-is-required</code> bundle key.
+     * Parameter {0} is the display name of the field.
+     * 
+     * @since 3.0
+     */
+
+    public void setRequiredMessage(String string)
+    {
+        _requiredMessage = string;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/DateValidator.java b/tapestry-framework/src/org/apache/tapestry/valid/DateValidator.java
new file mode 100644
index 0000000..74e17e9
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/DateValidator.java
@@ -0,0 +1,351 @@
+//  Copyright 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.tapestry.valid;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.form.IFormComponent;
+
+/**
+ *  Provides input validation for strings treated as dates.  In addition,
+ *  allows a minimum and maximum date to be set.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.8
+ *
+ **/
+
+public class DateValidator extends BaseValidator
+{
+    private DateFormat _format;
+    private String _displayFormat;
+    private Date _minimum;
+    private Date _maximum;
+    private Calendar _calendar;
+    private String _scriptPath = "/org/apache/tapestry/valid/DateValidator.script";
+
+    private static DateFormat defaultDateFormat = new SimpleDateFormat("MM/dd/yyyy");
+    private static final String defaultDateDisplayFormat = "MM/DD/YYYY";
+
+    private String _dateTooEarlyMessage;
+    private String _dateTooLateMessage;
+    private String _invalidDateFormatMessage;
+
+    public void setFormat(DateFormat value)
+    {
+        _format = value;
+    }
+
+    public DateFormat getFormat()
+    {
+        return _format;
+    }
+
+	/**
+	 * @return the {@link DateFormat} the validator will use, returning the default if no
+	 * other date format is specified via {@link #setFormat(DateFormat)}
+	 * 
+	 * @since 3.0
+	 */
+    public DateFormat getEffectiveFormat()
+    {
+        if (_format == null)
+            return defaultDateFormat;
+
+        return _format;
+    }
+
+    public String getDisplayFormat()
+    {
+        return _displayFormat;
+    }
+
+    public void setDisplayFormat(String value)
+    {
+        _displayFormat = value;
+    }
+
+	/**
+     * @return the display format message the validator will use, returning the default if no
+     * other display format message is specified.  The default is the {@link SimpleDateFormat#toPattern()}
+     * for {@link SimpleDateFormat}s, or "MM/DD/YYYY" for unknown {@link DateFormat} subclasses.
+     * 
+     * @since 3.0
+     */
+    public String getEffectiveDisplayFormat()
+    {
+        if (_displayFormat == null)
+        {
+            DateFormat format = getEffectiveFormat();
+            if (format instanceof SimpleDateFormat) 
+                return ((SimpleDateFormat)format).toPattern();
+            else
+                return defaultDateDisplayFormat;
+        }
+
+        return _displayFormat;
+    }
+
+    public String toString(IFormComponent file, Object value)
+    {
+        if (value == null)
+            return null;
+
+        Date date = (Date) value;
+
+        DateFormat format = getEffectiveFormat();
+
+        // DateFormat is not threadsafe, so guard access to it.
+
+        synchronized (format)
+        {
+            return format.format(date);
+        }
+    }
+
+    public Object toObject(IFormComponent field, String value) throws ValidatorException
+    {
+        if (checkRequired(field, value))
+            return null;
+
+        DateFormat format = getEffectiveFormat();
+
+        Date result;
+
+        try
+        {
+            // DateFormat is not threadsafe, so guard access
+            // to it.
+
+            synchronized (format)
+            {
+                result = format.parse(value);
+            }
+
+            if (_calendar == null)
+                _calendar = new GregorianCalendar();
+
+            _calendar.setTime(result);
+
+            // SimpleDateFormat allows two-digit dates to be
+            // entered, i.e., 12/24/66 is Dec 24 0066 ... that's
+            // probably not what is really wanted, so treat
+            // it as an invalid date.
+
+            if (_calendar.get(Calendar.YEAR) < 1000)
+                result = null;
+
+        }
+        catch (ParseException ex)
+        {
+            // ParseException does not include a useful error message
+            // about what's wrong.
+            result = null;
+        }
+
+        if (result == null)
+            throw new ValidatorException(
+                buildInvalidDateFormatMessage(field),
+                ValidationConstraint.DATE_FORMAT);
+
+        // OK, check that the date is in range.
+
+        if (_minimum != null && _minimum.compareTo(result) > 0)
+            throw new ValidatorException(
+                buildDateTooEarlyMessage(field, format.format(_minimum)),
+                ValidationConstraint.TOO_SMALL);
+
+        if (_maximum != null && _maximum.compareTo(result) < 0)
+            throw new ValidatorException(
+                buildDateTooLateMessage(field, format.format(_maximum)),
+                ValidationConstraint.TOO_LARGE);
+
+        return result;
+
+    }
+
+    public Date getMaximum()
+    {
+        return _maximum;
+    }
+
+    public void setMaximum(Date maximum)
+    {
+        _maximum = maximum;
+    }
+
+    public Date getMinimum()
+    {
+        return _minimum;
+    }
+
+    public void setMinimum(Date minimum)
+    {
+        _minimum = minimum;
+    }
+
+    /** 
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public void renderValidatorContribution(
+        IFormComponent field,
+        IMarkupWriter writer,
+        IRequestCycle cycle)
+    {
+        if (!(isClientScriptingEnabled() && isRequired()))
+            return;
+
+        Map symbols = new HashMap();
+
+        symbols.put("requiredMessage", buildRequiredMessage(field));
+
+        processValidatorScript(_scriptPath, cycle, field, symbols);
+    }
+
+    /**
+     *  @since 2.2
+     * 
+     **/
+
+    public String getScriptPath()
+    {
+        return _scriptPath;
+    }
+
+    /**
+     *  Allows a developer to use the existing validation logic with a different client-side
+     *  script.  This is often sufficient to allow application-specific error presentation
+     *  (perhaps by using DHTML to update the content of a &lt;span&gt; tag, or to use
+     *  a more sophisticated pop-up window than <code>window.alert()</code>).
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public void setScriptPath(String scriptPath)
+    {
+        _scriptPath = scriptPath;
+    }
+
+    /** @since 3.0 */
+
+    public String getDateTooEarlyMessage()
+    {
+        return _dateTooEarlyMessage;
+    }
+
+    /** @since 3.0 */
+
+    public String getDateTooLateMessage()
+    {
+        return _dateTooLateMessage;
+    }
+
+    /** @since 3.0 */
+
+    public String getInvalidDateFormatMessage()
+    {
+        return _invalidDateFormatMessage;
+    }
+
+    /** @since 3.0 */
+
+    protected String buildInvalidDateFormatMessage(IFormComponent field)
+    {
+        String pattern =
+            getPattern(
+                _invalidDateFormatMessage,
+                "invalid-date-format",
+                field.getPage().getLocale());
+
+        return formatString(pattern, field.getDisplayName(), getEffectiveDisplayFormat());
+    }
+
+    /** @since 3.0 **/
+
+    protected String buildDateTooEarlyMessage(IFormComponent field, String earliestDate)
+    {
+        String pattern =
+            getPattern(_dateTooEarlyMessage, "date-too-early", field.getPage().getLocale());
+
+        return formatString(pattern, field.getDisplayName(), earliestDate);
+    }
+
+    /** @since 3.0 */
+
+    protected String buildDateTooLateMessage(IFormComponent field, String latestDate)
+    {
+        String pattern =
+            getPattern(_dateTooLateMessage, "date-too-late", field.getPage().getLocale());
+
+        return formatString(pattern, field.getDisplayName(), latestDate);
+    }
+
+    /**
+     *  Overrides the bundle key
+     *  <code>date-too-early</code>.
+     *  Parameter {0} is the display name of the field.
+     *  Parameter {1} is the earliest allowed date.
+     * 
+     *  @since 3.0
+     */
+
+    public void setDateTooEarlyMessage(String string)
+    {
+        _dateTooEarlyMessage = string;
+    }
+
+    /**
+     *  Overrides the bundle key
+     *  <code>date-too-late</code>.
+     *  Parameter {0} is the display name of the field.
+     *  Parameter {1} is the latest allowed date.
+     * 
+     *  @since 3.0
+     */
+
+    public void setDateTooLateMessage(String string)
+    {
+        _dateTooLateMessage = string;
+    }
+
+    /**
+     *  Overrides the bundle key
+     *  <code>invalid-date-format</code>.
+     *  Parameter {0} is the display name of the field.
+     *  Parameter {1} is the allowed format.
+     * 
+     *  @since 3.0
+     */
+
+    public void setInvalidDateFormatMessage(String string)
+    {
+        _invalidDateFormatMessage = string;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/DateValidator.script b/tapestry-framework/src/org/apache/tapestry/valid/DateValidator.script
new file mode 100644
index 0000000..6ec5098
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/DateValidator.script
@@ -0,0 +1,43 @@
+<?xml version="1.0"?>
+<!-- $Id$ -->
+<!DOCTYPE script PUBLIC
+	"-//Apache Software Foundation//Tapestry Script Specification 3.0//EN"
+	"http://jakarta.apache.org/tapestry/dtd/Script_3_0.dtd">
+	
+<!-- 
+
+  Creates a script for validating that a date field is required.  Eventually,
+  this will also do client-side input validation.
+  
+  Input symbols:
+     field, form, validator:  As normal for a validation script.
+	 requiredMessage: Message to display if the field is required yet blank.
+     
+-->
+	
+<script>
+
+<include-script resource-path="/org/apache/tapestry/valid/Validator.js"/>
+
+<input-symbol key="field" class="org.apache.tapestry.valid.ValidField" required="yes"/>
+<input-symbol key="form" class="org.apache.tapestry.IForm" required="yes"/>
+<input-symbol key="validator" class="org.apache.tapestry.valid.DateValidator" required="yes"/>
+<input-symbol key="requiredMessage" class="java.lang.String"/>
+
+<let key="function" unique="yes">
+validate_${field.name}
+</let>
+
+<body>
+function ${function}()
+{
+  var field = document.${form.name}.${field.name}; 
+
+  if (field.value.length == 0)
+     return validator_invalid_field(field, "${requiredMessage}");
+
+  return true;
+}
+</body>
+
+</script>
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/EmailValidator.java b/tapestry-framework/src/org/apache/tapestry/valid/EmailValidator.java
new file mode 100644
index 0000000..303e3cc
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/EmailValidator.java
@@ -0,0 +1,218 @@
+//  Copyright 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.tapestry.valid;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.form.IFormComponent;
+
+/**
+ *  Simple validation of email strings, to enforce required, and minimum length
+ *  (maximum length is enforced in the client browser, by setting a maximum input
+ *  length on the text field).
+ *
+ *
+ *  @author Malcolm Edgar
+ *  @version $Id$
+ *  @since 2.3
+ *
+ **/
+
+public class EmailValidator extends BaseValidator
+{
+    private int _minimumLength;
+    private String _minimumLengthMessage;
+    private String _invalidEmailFormatMessage;
+
+    private String _scriptPath = "/org/apache/tapestry/valid/EmailValidator.script";
+
+    public EmailValidator()
+    {
+    }
+
+    private EmailValidator(boolean required)
+    {
+        super(required);
+    }
+
+    public String toString(IFormComponent field, Object value)
+    {
+        if (value == null)
+            return null;
+
+        return value.toString();
+    }
+
+    public Object toObject(IFormComponent field, String input) throws ValidatorException
+    {
+        if (checkRequired(field, input))
+            return null;
+
+        if (_minimumLength > 0 && input.length() < _minimumLength)
+            throw new ValidatorException(
+                buildMinimumLengthMessage(field),
+                ValidationConstraint.MINIMUM_WIDTH);
+
+        if (!isValidEmail(input))
+            throw new ValidatorException(
+                buildInvalidEmailFormatMessage(field),
+                ValidationConstraint.EMAIL_FORMAT);
+
+        return input;
+    }
+
+    public int getMinimumLength()
+    {
+        return _minimumLength;
+    }
+
+    public void setMinimumLength(int minimumLength)
+    {
+        _minimumLength = minimumLength;
+    }
+
+    public void renderValidatorContribution(
+        IFormComponent field,
+        IMarkupWriter writer,
+        IRequestCycle cycle)
+    {
+        if (!isClientScriptingEnabled())
+            return;
+
+        Map symbols = new HashMap();
+
+        Locale locale = field.getPage().getLocale();
+        String displayName = field.getDisplayName();
+
+        if (isRequired())
+            symbols.put("requiredMessage", buildRequiredMessage(field));
+
+        if (_minimumLength > 0)
+            symbols.put("minimumLengthMessage", buildMinimumLengthMessage(field));
+
+        String pattern = getPattern(getInvalidEmailFormatMessage(), "invalid-email-format", locale);
+
+        symbols.put("emailFormatMessage", formatString(pattern, displayName));
+
+        processValidatorScript(_scriptPath, cycle, field, symbols);
+    }
+
+    public String getScriptPath()
+    {
+        return _scriptPath;
+    }
+
+    /**
+     *  Allows a developer to use the existing validation logic with a different client-side
+     *  script.  This is often sufficient to allow application-specific error presentation
+     *  (perhaps by using DHTML to update the content of a &lt;span&gt; tag, or to use
+     *  a more sophisticated pop-up window than <code>window.alert()</code>).
+     * 
+     **/
+
+    public void setScriptPath(String scriptPath)
+    {
+        _scriptPath = scriptPath;
+    }
+
+    /**
+     *  Return true if the email format is valid.
+     * 
+     *  @param email the email string to validate
+     *  @return true if the email format is valid
+     */
+
+    protected boolean isValidEmail(String email)
+    {
+        int atIndex = email.indexOf('@');
+
+        if ((atIndex == -1) || (atIndex == 0) || (atIndex == email.length() - 1))
+        {
+            return false;
+        }
+        else
+        {
+            return true;
+        }
+    }
+
+    /** @since 3.0 */
+
+    public String getInvalidEmailFormatMessage()
+    {
+        return _invalidEmailFormatMessage;
+    }
+
+    /** @since 3.0 */
+
+    public String getMinimumLengthMessage()
+    {
+        return _minimumLengthMessage;
+    }
+
+    /**
+     *  Overrides the <code>invalid-email-format</code>
+     *  bundle key.
+     *  Parameter {0} is the display name of the field.
+     * 
+     *  @since 3.0
+     * 
+     */
+
+    public void setInvalidEmailFormatMessage(String string)
+    {
+        _invalidEmailFormatMessage = string;
+    }
+
+    /**
+     *  Overrides the <code>field-too-short</code> bundle key.
+     *  Parameter {0} is the minimum length.
+     *  Parameter {1} is the display name of the field.
+     * 
+     *  @since 3.0
+     * 
+     */
+    public void setMinimumLengthMessage(String string)
+    {
+        _minimumLengthMessage = string;
+    }
+
+    /** @since 3.0 */
+
+    protected String buildMinimumLengthMessage(IFormComponent field)
+    {
+        String pattern =
+            getPattern(_minimumLengthMessage, "field-too-short", field.getPage().getLocale());
+
+        return formatString(pattern, Integer.toString(_minimumLength), field.getDisplayName());
+    }
+
+    /** @since 3.0 */
+
+    protected String buildInvalidEmailFormatMessage(IFormComponent field)
+    {
+        String pattern =
+            getPattern(
+                _invalidEmailFormatMessage,
+                "invalid-email-format",
+                field.getPage().getLocale());
+
+        return formatString(pattern, field.getDisplayName());
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/EmailValidator.script b/tapestry-framework/src/org/apache/tapestry/valid/EmailValidator.script
new file mode 100644
index 0000000..4f724b3
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/EmailValidator.script
@@ -0,0 +1,66 @@
+<?xml version="1.0"?>
+<!-- $Id$ -->
+<!DOCTYPE script PUBLIC
+	"-//Apache Software Foundation//Tapestry Script Specification 3.0//EN"
+	"http://jakarta.apache.org/tapestry/dtd/Script_3_0.dtd">
+
+<!--
+
+  Creates a script for validating that a field is required and/or has a minimum
+  field length.
+
+  Input symbols:
+     field, form, validator:  As normal for a validation script.
+	 requiredMessage: Message to display if the field is required yet blank.
+	 minimumLengthMessage: Message to display if the field length is too short.
+
+-->
+
+<script>
+
+<include-script resource-path="/org/apache/tapestry/valid/Validator.js"/>
+
+<input-symbol key="field" class="org.apache.tapestry.valid.ValidField" required="yes"/>
+<input-symbol key="form" class="org.apache.tapestry.IForm" required="yes"/>
+<input-symbol key="validator" class="org.apache.tapestry.valid.EmailValidator" required="yes"/>
+<input-symbol key="requiredMessage" class="java.lang.String"/>
+<input-symbol key="minimumLengthMessage" class="java.lang.String"/>
+<input-symbol key="emailFormatMessage" class="java.lang.String"/>
+
+<let key="function" unique="yes">
+validate_${field.name}
+</let>
+
+<body>
+function ${function}()
+{
+  var field = document.${form.name}.${field.name};
+
+  strValue = field.value.replace(/ /g,"");
+
+  field.value = strValue;
+
+<if expression="validator.required">
+  if (strValue.length == 0)
+     return validator_invalid_field(field, "${requiredMessage}");
+</if>
+
+<if-not expression="validator.required">
+  if (strValue.length == 0)
+     return true;
+</if-not>
+
+<if expression="validator.minimumLength">
+  if (strValue.length &lt; ${validator.minimumLength})
+     return validator_invalid_field(field, "${minimumLengthMessage}");
+</if>
+
+  atIndex = strValue.indexOf("@");
+  if ((atIndex == -1) || (atIndex == 0) || (atIndex == strValue.length -1))
+     return validator_invalid_field(field, "${emailFormatMessage}");
+
+  return true;
+}
+</body>
+
+</script>
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/FieldLabel.java b/tapestry-framework/src/org/apache/tapestry/valid/FieldLabel.java
new file mode 100644
index 0000000..b8c3426
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/FieldLabel.java
@@ -0,0 +1,109 @@
+//  Copyright 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.tapestry.valid;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.BindingException;
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.form.Form;
+import org.apache.tapestry.form.IFormComponent;
+
+/**
+ *  Used to label an {@link IFormComponent}.  Because such fields
+ *  know their displayName (user-presentable name), there's no reason
+ *  to hard code the label in a page's HTML template (this also helps
+ *  with localization).
+ *
+ *  [<a href="../../../../../ComponentReference/FieldLabel.html">Component Reference</a>]
+
+ *
+ *  @author Howard Lewis Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public abstract class FieldLabel extends AbstractComponent
+{
+    /**
+    *  Gets the {@link IFormComponent}
+    *  and {@link IValidationDelegate delegate},
+    *  then renders the label obtained from the field.  Does nothing
+    *  when rewinding.
+    *
+    **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        if (cycle.isRewinding())
+            return;
+
+        IFormComponent field = getField();
+        String displayName = getDisplayName();
+
+        if (displayName == null)
+        {
+            if (field == null)
+                throw Tapestry.createRequiredParameterException(this, "field");
+
+            displayName = field.getDisplayName();
+
+            if (displayName == null)
+            {
+                String msg = Tapestry.format("FieldLabel.no-display-name", field.getExtendedId());
+
+                throw new BindingException(msg, this, null, getBinding("field"), null);
+            }
+        }
+
+        IForm form = Form.get(cycle);
+
+        if (form == null)
+        {
+            String msg = Tapestry.getMessage("FieldLabel.must-be-contained-by-form");
+
+            throw new ApplicationRuntimeException(msg, this, null, null);
+        }
+
+        IValidationDelegate delegate = form.getDelegate();
+
+        if (delegate == null)
+        {
+            String msg =
+                Tapestry.format("FieldLabel.no-delegate", getExtendedId(), form.getExtendedId());
+
+            throw new ApplicationRuntimeException(msg, this, null, null);
+        }
+
+        delegate.writeLabelPrefix(field, writer, cycle);
+
+        if (getRaw()) {
+            writer.printRaw(displayName);
+        } else {
+            writer.print(displayName);
+        }
+
+        delegate.writeLabelSuffix(field, writer, cycle);
+    }
+
+    public abstract String getDisplayName();
+
+    public abstract IFormComponent getField();
+
+    public abstract boolean getRaw();
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/FieldLabel.jwc b/tapestry-framework/src/org/apache/tapestry/valid/FieldLabel.jwc
new file mode 100644
index 0000000..a79043b
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/FieldLabel.jwc
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN"
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.valid.FieldLabel"
+	allow-body="no"
+	allow-informal-parameters="no">
+
+  <description>
+  Labels a ValidField.
+  </description>
+
+  <parameter name="field" type="org.apache.tapestry.form.IFormComponent" required="yes" direction="in"/>
+
+  <parameter name="displayName" type="java.lang.String" direction="in">
+      <description>
+          Optional.  Defaults to the displayName of the associated field.
+      </description>
+  </parameter>
+
+  <parameter name="raw" type="boolean" direction="in">
+      <description>
+        If false (the default), then HTML characters in the value are escaped.  If
+        true, then value is emitted exactly as is.
+      </description>
+  </parameter>
+
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/FieldTracking.java b/tapestry-framework/src/org/apache/tapestry/valid/FieldTracking.java
new file mode 100644
index 0000000..6471979
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/FieldTracking.java
@@ -0,0 +1,106 @@
+//  Copyright 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.tapestry.valid;
+
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.form.IFormComponent;
+
+/**
+ *  Default implementation of {@link IFieldTracking}.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.8
+ *
+ **/
+
+public class FieldTracking implements IFieldTracking
+{
+    private IFormComponent _component;
+    private String _input;
+    private IRender _renderer;
+    private String _fieldName;
+    private ValidationConstraint _constraint;
+
+	/**
+	 *  Constructor used for unassociated errors; errors that are not about any particular
+	 *  field within the form.
+	 * 
+	 **/
+	
+    FieldTracking()
+    {
+    }
+
+	/**
+	 *  Standard constructor for a field (with the given name), rendered
+	 *  by the specified component.
+	 * 
+	 **/
+	
+    FieldTracking(String fieldName, IFormComponent component)
+    {
+        _fieldName = fieldName;
+        _component = component;
+    }
+
+    public IFormComponent getComponent()
+    {
+        return _component;
+    }
+
+    public IRender getErrorRenderer()
+    {
+        return _renderer;
+    }
+
+    public void setErrorRenderer(IRender value)
+    {
+        _renderer = value;
+    }
+
+    public String getInput()
+    {
+        return _input;
+    }
+
+    public void setInput(String value)
+    {
+        _input = value;
+    }
+
+    public String getFieldName()
+    {
+        return _fieldName;
+    }
+
+    public ValidationConstraint getConstraint()
+    {
+        return _constraint;
+    }
+
+    public void setConstraint(ValidationConstraint constraint)
+    {
+        _constraint = constraint;
+    }
+
+    /** @since 3.0 **/
+
+    public boolean isInError()
+    {
+        return _renderer != null;
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/IFieldTracking.java b/tapestry-framework/src/org/apache/tapestry/valid/IFieldTracking.java
new file mode 100644
index 0000000..cdf509b
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/IFieldTracking.java
@@ -0,0 +1,80 @@
+//  Copyright 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.tapestry.valid;
+
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.form.IFormComponent;
+
+/**
+ * Defines the interface for an object that tracks input fields. This interface is now poorly named,
+ * in that it tracks errors that may <em>not</em> be associated with a specific field.
+ * <p>
+ * For each field, a flag is stored indicating if the field is, in fact, in error. The input
+ * supplied by the client is stored so that if the form is re-rendered (as is typically done when
+ * there are input errors), the value entered by the user is displayed back to the user. An error
+ * message renderer is stored; this is an object that can render the error message (it is usually a
+ * {@link org.apache.tapestry.valid.RenderString}wrapper around a simple string).
+ * 
+ * @author Howard Lewis Ship
+ * @version $Id$
+ * @since 1.0.8
+ */
+
+public interface IFieldTracking
+{
+    /**
+     * Returns true if the field is in error (that is, if it has an error message
+     * {@link #getErrorRenderer() renderer}.
+     */
+
+    public boolean isInError();
+
+    /**
+     * Returns the field component. This may return null if the error is not associated with any
+     * particular field.
+     */
+
+    public IFormComponent getComponent();
+
+    /**
+     * Returns an object that will render the error message. Alternately, the
+     * <code>toString()</code> of the renderer can be used as a simple error message.
+     * 
+     * @since 1.0.9
+     */
+
+    public IRender getErrorRenderer();
+
+    /**
+     * Returns the invalid input recorded for the field. This is stored so that, on a subsequent
+     * render, the smae invalid input can be presented to the client to be corrected.
+     */
+
+    public String getInput();
+
+    /**
+     * Returns the name of the field, that is, the name assigned by the form (this will differ from
+     * the component's id when any kind of looping operation is in effect).
+     */
+
+    public String getFieldName();
+
+    /**
+     * Returns the validation constraint that was violated by the input. This may be null if the
+     * constraint isn't known.
+     */
+
+    public ValidationConstraint getConstraint();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/IValidationDelegate.java b/tapestry-framework/src/org/apache/tapestry/valid/IValidationDelegate.java
new file mode 100644
index 0000000..c869537
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/IValidationDelegate.java
@@ -0,0 +1,250 @@
+//  Copyright 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.tapestry.valid;
+
+import java.util.List;
+
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.form.IFormComponent;
+
+/**
+ * Interface used to track validation errors in forms and {@link IFormComponent}s (including
+ * {@link org.apache.tapestry.form.AbstractTextField}and its subclasses).
+ * <p>
+ * In addition, controls how fields that are in error are presented (they can be marked in various
+ * ways by the delegate; the default implementation adds two red asterisks to the right of the
+ * field).
+ * <p>
+ * The interface is designed so that a single instance can be shared with many instances of
+ * {@link IFormComponent}.
+ * <p>
+ * Starting with release 1.0.8, this interface was extensively revised (in a non-backwards
+ * compatible way) to move the tracking of errors and invalid values (during a request cycle) to the
+ * delegate. It has evolved from a largely stateless conduit for error messages into a very stateful
+ * tracker of field state.
+ * <p>
+ * Starting with release 1.0.9, this interface was <em>again</em> reworked, to allow tracking of
+ * errors in {@link IFormComponent form components}, and to allow unassociated (with any field)
+ * errors to be tracked.
+ * <p>
+ * <b>Fields vs. Form Components </b> <br>
+ * For most simple forms, these terms are pretty much synonymous. Your form will render normally,
+ * and each form component will render only once. Some of your form components will be
+ * {@link ValidField}components and handle most of their validation internally (with the help of
+ * {@link IValidator}objects). In addition, your form listener may do additional validation and
+ * notify the validation delegate of additional errors, some of which are associated with a
+ * particular field, some of which are unassociated with any particular field.
+ * <p>
+ * But what happens if you use a {@link org.apache.tapestry.components.Foreach}or
+ * {@link org.apache.tapestry.form.ListEdit}inside your form? Some of your components will render
+ * multiple times. In this case you will have multiple <em>fields</em>. Each field will have a
+ * unique field name (you can see this in the generated HTML). It is this field name that the
+ * delegate keys off of, which means that some fields generated by a component may have errors and
+ * some may not, it all works fine (with one exception).
+ * <p>
+ * <b>The Exception </b> <br>
+ * The problem is that a component doesn't know its field name until its <code>render()</code>
+ * method is invoked (at which point, it allocates a unique field name from the
+ * {@link org.apache.tapestry.IForm#getElementId(org.apache.tapestry.form.IFormComponent)}. This is
+ * not a problem for the field or its {@link IValidator}, but screws things up for the
+ * {@link FieldLabel}.
+ * <p>
+ * Typically, the label is rendered <em>before</em> the corresponding form component. Form
+ * components leave their last assigned field name in their
+ * {@link IFormComponent#getName() name property}. So if the form component is in any kind of loop,
+ * the {@link FieldLabel}will key its name, {@link IFormComponent#getDisplayName() display name}
+ * and error status off of its last renderred value. So the moral of the story is don't use
+ * {@link FieldLabel}in this situation.
+ * 
+ * @author Howard Lewis Ship
+ */
+
+public interface IValidationDelegate
+{
+    /**
+     * Invoked before other methods to configure the delegate for the given form component. Sets the
+     * current field based on the {@link IFormComponent#getName() name}of the form component (which
+     * is almost always a {@link ValidField}).
+     * <p>
+     * The caller should invoke this with a parameter of null to record unassociated global errors
+     * (errors not associated with any particular field).
+     * 
+     * @since 1.0.8
+     */
+
+    public void setFormComponent(IFormComponent component);
+
+    /**
+     * Returns true if the current component is in error (that is, had bad input submitted by the
+     * end user).
+     * 
+     * @since 1.0.8
+     */
+
+    public boolean isInError();
+
+    /**
+     * Returns the string submitted by the client as the value for the current field.
+     * 
+     * @since 1.0.8
+     */
+
+    public String getFieldInputValue();
+
+    /**
+     * Returns a {@link List}of {@link IFieldTracking}, in default order (the order in which
+     * fields are renderred). A caller should not change the values (the List is immutable). May
+     * return null if no fields are in error.
+     * 
+     * @since 1.0.8
+     */
+
+    public List getFieldTracking();
+
+    /**
+     * Resets any tracking information for the current field. This will clear the field's inError
+     * flag, and set its error message and invalid input value to null.
+     * 
+     * @since 1.0.8
+     */
+
+    public void reset();
+
+    /**
+     * Clears all tracking information.
+     * 
+     * @since 1.0.10
+     */
+
+    public void clear();
+
+    /**
+     * Clears all errors, but maintains user input. This is useful when a form has been submitted
+     * for a semantic other than "process this data". A common example of this is a dependent drop
+     * down list; selecting an option in one drop down list forces a submit to repopulate the
+     * options in a second, dependent drop down list.
+     * <p>
+     * In these cases, the user input provided in the request is maintained, but any errors should
+     * be cleared out (to prevent unwanted error messages and decorations).
+     * 
+     * @since 3.0.1
+     */
+
+    public void clearErrors();
+
+    /**
+     * Records the user's input for the current form component. Input should be recorded even if
+     * there isn't an explicit error, since later form-wide validations may discover an error in the
+     * field.
+     * 
+     * @since 3.0
+     */
+
+    public void recordFieldInputValue(String input);
+
+    /**
+     * The error notification method, invoked during the rewind phase (that is, while HTTP
+     * parameters are being extracted from the request and assigned to various object properties).
+     * <p>
+     * Typically, the delegate simply invokes {@link #record(String, ValidationConstraint)}or
+     * {@link #record(IRender, ValidationConstraint)}, but special delegates may override this
+     * behavior to provide (in some cases) different error messages or more complicated error
+     * renderers.
+     */
+
+    public void record(ValidatorException ex);
+
+    /**
+     * Records an error in the current field, or an unassociated error if there is no current field.
+     * 
+     * @param message
+     *            message to display (@see RenderString}
+     * @param constraint
+     *            the constraint that was violated, or null if not known
+     * @since 1.0.9
+     */
+
+    public void record(String message, ValidationConstraint constraint);
+
+    /**
+     * Records an error in the current component, or an unassociated error. The maximum flexibility
+     * recorder.
+     * 
+     * @param errorRenderer
+     *            object that will render the error message (@see RenderString}. The object should
+     *            implement a reasonable <code>toString()</code> as well, to allow the error
+     *            message to be rendered using an Insert component, or used where full markup is not
+     *            allowed.
+     * @param constraint
+     *            the constraint that was violated, or null if not known
+     */
+
+    public void record(IRender errorRenderer, ValidationConstraint constraint);
+
+    /**
+     * Invoked before the field is rendered. If the field is in error, the delegate may decorate the
+     * field in some way (to highlight its error state).
+     */
+
+    public void writePrefix(IMarkupWriter writer, IRequestCycle cycle, IFormComponent component,
+            IValidator validator);
+
+    /**
+     * Invoked just before the &lt;input&gt; element is closed. The delegate can write additional
+     * attributes. This is often used to set the CSS class of the field so that it can be displayed
+     * differently, if in error (or required).
+     * 
+     * @since 1.0.5
+     */
+
+    public void writeAttributes(IMarkupWriter writer, IRequestCycle cycle,
+            IFormComponent component, IValidator validator);
+
+    /**
+     * Invoked after the form component is rendered, so that the delegate may decorate the form
+     * component (if it is in error).
+     */
+
+    public void writeSuffix(IMarkupWriter writer, IRequestCycle cycle, IFormComponent component,
+            IValidator validator);
+
+    /**
+     * Invoked by a {@link FieldLabel}just before writing the name of the form component.
+     */
+
+    public void writeLabelPrefix(IFormComponent component, IMarkupWriter writer, IRequestCycle cycle);
+
+    /**
+     * Invoked by a {@link FieldLabel}just after writing the name of the form component.
+     */
+
+    public void writeLabelSuffix(IFormComponent component, IMarkupWriter writer, IRequestCycle cycle);
+
+    /**
+     * Returns true if any form component has errors.
+     */
+
+    public boolean getHasErrors();
+
+    /**
+     * Returns the {@link IFieldTracking}for the current component, if any. Useful when displaying
+     * error messages for individual fields.
+     * 
+     * @since 3.0.2
+     */
+    public IFieldTracking getCurrentFieldTracking();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/IValidator.java b/tapestry-framework/src/org/apache/tapestry/valid/IValidator.java
new file mode 100644
index 0000000..5d636d4
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/IValidator.java
@@ -0,0 +1,79 @@
+//  Copyright 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.tapestry.valid;
+
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.form.IFormComponent;
+
+/**
+ *  An object that works with an {@link IFormComponent} to format output
+ *  (convert object values to strings values) and to process input
+ *  (convert strings to object values and validate them).
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.8
+ *
+ **/
+
+public interface IValidator
+{
+    /**
+     *  All validators must implement a required property.  If true,
+     *  the client must supply a non-null value.
+     *
+     **/
+
+    public boolean isRequired();
+
+    /**
+     *  Invoked during rendering to convert an object value (which may be null)
+     *  to a String.  It is acceptible to return null.  The string will be the
+     *  VALUE attribute of the HTML text field.
+     *
+     **/
+
+    public String toString(IFormComponent field, Object value);
+
+    /**
+     *  Converts input, submitted by the client, into an object value.
+     *  May return null if the input is null (and the required flag is false).
+     *
+     *  <p>The input string will already have been trimmed.  It may be null.
+     *
+     *  @throws ValidatorException if the string cannot be converted into
+     *  an object, or the object is
+     *  not valid (due to other constraints).
+     **/
+
+    public Object toObject(IFormComponent field, String input) throws ValidatorException;
+
+    /**
+     *  Invoked by the field after it finishes rendering its tag (but before
+     *  the tag is closed) to allow the validator to provide a contribution to the
+     *  rendering process.  Validators typically generated client-side JavaScript
+     *  to peform validation.
+     *
+     *  @since 2.2
+     *
+     **/
+
+    public void renderValidatorContribution(
+        IFormComponent field,
+        IMarkupWriter writer,
+        IRequestCycle cycle);
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/NumberValidator.java b/tapestry-framework/src/org/apache/tapestry/valid/NumberValidator.java
new file mode 100644
index 0000000..c48fb45
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/NumberValidator.java
@@ -0,0 +1,682 @@
+//  Copyright 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.tapestry.valid;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.form.IFormComponent;
+import org.apache.tapestry.util.AdaptorRegistry;
+
+/**
+ *  Simple validation for standard number classes.  This is probably insufficient
+ *  for anything tricky and application specific, such as parsing currency.  
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.8
+ *
+ **/
+
+public class NumberValidator extends BaseValidator
+{
+    private static final Map TYPES = new HashMap();
+
+    static {
+        TYPES.put("boolean", boolean.class);
+        TYPES.put("Boolean", Boolean.class);
+        TYPES.put("java.lang.Boolean", Boolean.class);
+        TYPES.put("char", char.class);
+        TYPES.put("Character", Character.class);
+        TYPES.put("java.lang.Character", Character.class);
+        TYPES.put("short", short.class);
+        TYPES.put("Short", Short.class);
+        TYPES.put("java.lang.Short", Short.class);
+        TYPES.put("int", int.class);
+        TYPES.put("Integer", Integer.class);
+        TYPES.put("java.lang.Integer", Integer.class);
+        TYPES.put("long", long.class);
+        TYPES.put("Long", Long.class);
+        TYPES.put("java.lang.Long", Long.class);
+        TYPES.put("float", float.class);
+        TYPES.put("Float", Float.class);
+        TYPES.put("java.lang.Float", Float.class);
+        TYPES.put("byte", byte.class);
+        TYPES.put("Byte", Byte.class);
+        TYPES.put("java.lang.Byte", Byte.class);
+        TYPES.put("double", double.class);
+        TYPES.put("Double", Double.class);
+        TYPES.put("java.lang.Double", Double.class);
+        TYPES.put("java.math.BigInteger", BigInteger.class);
+        TYPES.put("java.math.BigDecimal", BigDecimal.class);
+    }
+
+    private static final Set INT_TYPES = new HashSet();
+
+    private Class _valueTypeClass = int.class;
+
+    private boolean _zeroIsNull;
+    private Number _minimum;
+    private Number _maximum;
+
+    private String _scriptPath = "/org/apache/tapestry/valid/NumberValidator.script";
+
+    private String _invalidNumericFormatMessage;
+    private String _invalidIntegerFormatMessage;
+    private String _numberTooSmallMessage;
+    private String _numberTooLargeMessage;
+    private String _numberRangeMessage;
+
+    private static AdaptorRegistry _numberAdaptors = new AdaptorRegistry();
+
+    public final static int NUMBER_TYPE_INTEGER = 0;
+	public final static int NUMBER_TYPE_REAL = 1;
+
+	/**
+	 * This class is not meant for use outside of NumberValidator; it
+	 * is public only to fascilitate some unit testing.
+	 * 
+	 */
+    public static abstract class NumberAdaptor
+    {
+        /**
+         *  Parses a non-empty {@link String} into the correct subclass of
+         *  {@link Number}.
+         *
+         *  @throws NumberFormatException if the String can not be parsed.
+         **/
+
+        abstract public Number parse(String value);
+
+        /**
+         *  Indicates the type of the number represented -- integer or real.
+         *  The information is used to build the client-side validator.  
+         *  This method could return a boolean, but returns an int to allow
+         *  future extensions of the validator.
+         *   
+         *  @return one of the predefined number types
+         **/
+        abstract public int getNumberType();
+
+        public int compare(Number left, Number right)
+        {
+            if (!left.getClass().equals(right.getClass()))
+                right = coerce(right);
+
+            Comparable lc = (Comparable) left;
+
+            return lc.compareTo(right);
+        }
+
+        /**
+         * Invoked when comparing two Numbers of different types.
+         * The number is cooerced from its ordinary type to 
+         * the correct type for comparison.
+         * 
+         * @since 3.0
+         */
+        protected abstract Number coerce(Number number);
+    }
+
+    private static abstract class IntegerNumberAdaptor extends NumberAdaptor
+    {
+        public int getNumberType()
+        {
+            return NUMBER_TYPE_INTEGER;
+        }
+    }
+
+    private static abstract class RealNumberAdaptor extends NumberAdaptor
+    {
+        public int getNumberType()
+        {
+            return NUMBER_TYPE_REAL;
+        }
+    }
+
+    private static class ByteAdaptor extends IntegerNumberAdaptor
+    {
+        public Number parse(String value)
+        {
+            return new Byte(value);
+        }
+
+        protected Number coerce(Number number)
+        {
+            return new Byte(number.byteValue());
+        }
+    }
+
+    private static class ShortAdaptor extends IntegerNumberAdaptor
+    {
+        public Number parse(String value)
+        {
+            return new Short(value);
+        }
+
+        protected Number coerce(Number number)
+        {
+            return new Short(number.shortValue());
+        }
+    }
+
+    private static class IntAdaptor extends IntegerNumberAdaptor
+    {
+        public Number parse(String value)
+        {
+            return new Integer(value);
+        }
+
+        protected Number coerce(Number number)
+        {
+            return new Integer(number.intValue());
+        }
+    }
+
+    private static class LongAdaptor extends IntegerNumberAdaptor
+    {
+        public Number parse(String value)
+        {
+            return new Long(value);
+        }
+
+        protected Number coerce(Number number)
+        {
+            return new Long(number.longValue());
+        }
+    }
+
+    private static class FloatAdaptor extends RealNumberAdaptor
+    {
+        public Number parse(String value)
+        {
+            return new Float(value);
+        }
+
+        protected Number coerce(Number number)
+        {
+            return new Float(number.floatValue());
+        }
+    }
+
+    private static class DoubleAdaptor extends RealNumberAdaptor
+    {
+        public Number parse(String value)
+        {
+            return new Double(value);
+        }
+
+        protected Number coerce(Number number)
+        {
+            return new Double(number.doubleValue());
+        }
+    }
+
+    private static class BigDecimalAdaptor extends RealNumberAdaptor
+    {
+        public Number parse(String value)
+        {
+            return new BigDecimal(value);
+        }
+
+        protected Number coerce(Number number)
+        {
+            return new BigDecimal(number.doubleValue());
+        }
+    }
+
+    private static class BigIntegerAdaptor extends IntegerNumberAdaptor
+    {
+        public Number parse(String value)
+        {
+            return new BigInteger(value);
+        }
+
+        protected Number coerce(Number number)
+        {
+            return new BigInteger(number.toString());
+        }
+    }
+
+    static {
+        NumberAdaptor byteAdaptor = new ByteAdaptor();
+        NumberAdaptor shortAdaptor = new ShortAdaptor();
+        NumberAdaptor intAdaptor = new IntAdaptor();
+        NumberAdaptor longAdaptor = new LongAdaptor();
+        NumberAdaptor floatAdaptor = new FloatAdaptor();
+        NumberAdaptor doubleAdaptor = new DoubleAdaptor();
+
+        _numberAdaptors.register(Byte.class, byteAdaptor);
+        _numberAdaptors.register(byte.class, byteAdaptor);
+        _numberAdaptors.register(Short.class, shortAdaptor);
+        _numberAdaptors.register(short.class, shortAdaptor);
+        _numberAdaptors.register(Integer.class, intAdaptor);
+        _numberAdaptors.register(int.class, intAdaptor);
+        _numberAdaptors.register(Long.class, longAdaptor);
+        _numberAdaptors.register(long.class, longAdaptor);
+        _numberAdaptors.register(Float.class, floatAdaptor);
+        _numberAdaptors.register(float.class, floatAdaptor);
+        _numberAdaptors.register(Double.class, doubleAdaptor);
+        _numberAdaptors.register(double.class, doubleAdaptor);
+
+        _numberAdaptors.register(BigDecimal.class, new BigDecimalAdaptor());
+        _numberAdaptors.register(BigInteger.class, new BigIntegerAdaptor());
+    }
+
+    public String toString(IFormComponent field, Object value)
+    {
+        if (value == null)
+            return null;
+
+        if (_zeroIsNull)
+        {
+            Number number = (Number) value;
+
+            if (number.doubleValue() == 0.0)
+                return null;
+        }
+
+        return value.toString();
+    }
+
+    private NumberAdaptor getAdaptor(IFormComponent field)
+    {
+        NumberAdaptor result = getAdaptor(_valueTypeClass);
+
+        if (result == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "NumberValidator.no-adaptor-for-field",
+                    field,
+                    _valueTypeClass.getName()));
+
+        return result;
+    }
+
+	/**
+	 * Returns an adaptor for the given type.
+	 * 
+	 * <p>
+	 * Note: this method exists only for testing purposes. It is not meant to
+	 * be invoked by user code and is subject to change at any time.
+	 * 
+	 * @param type the type (a Number subclass) for which to return an adaptor
+	 * @return the adaptor, or null if no such adaptor may be found
+	 * @since 3.0
+	 */
+    public static NumberAdaptor getAdaptor(Class type)
+    {
+        return (NumberAdaptor) _numberAdaptors.getAdaptor(type);
+    }
+
+    public Object toObject(IFormComponent field, String value) throws ValidatorException
+    {
+        if (checkRequired(field, value))
+            return null;
+
+        NumberAdaptor adaptor = getAdaptor(field);
+        Number result = null;
+
+        try
+        {
+            result = adaptor.parse(value);
+        }
+        catch (NumberFormatException ex)
+        {
+            throw new ValidatorException(
+                buildInvalidNumericFormatMessage(field),
+                ValidationConstraint.NUMBER_FORMAT);
+        }
+
+        if (_minimum != null && adaptor.compare(result, _minimum) < 0)
+            throw new ValidatorException(
+                buildNumberTooSmallMessage(field),
+                ValidationConstraint.TOO_SMALL);
+
+        if (_maximum != null && adaptor.compare(result, _maximum) > 0)
+            throw new ValidatorException(
+                buildNumberTooLargeMessage(field),
+                ValidationConstraint.TOO_LARGE);
+
+        return result;
+    }
+
+    public Number getMaximum()
+    {
+        return _maximum;
+    }
+
+    public boolean getHasMaximum()
+    {
+        return _maximum != null;
+    }
+
+    public void setMaximum(Number maximum)
+    {
+        _maximum = maximum;
+    }
+
+    public Number getMinimum()
+    {
+        return _minimum;
+    }
+
+    public boolean getHasMinimum()
+    {
+        return _minimum != null;
+    }
+
+    public void setMinimum(Number minimum)
+    {
+        _minimum = minimum;
+    }
+
+    /**
+     *  If true, then when rendering, a zero is treated as a non-value, and null is returned.
+     *  If false, the default, then zero is rendered as zero.
+     * 
+     **/
+
+    public boolean getZeroIsNull()
+    {
+        return _zeroIsNull;
+    }
+
+    public void setZeroIsNull(boolean zeroIsNull)
+    {
+        _zeroIsNull = zeroIsNull;
+    }
+
+    /** 
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public void renderValidatorContribution(
+        IFormComponent field,
+        IMarkupWriter writer,
+        IRequestCycle cycle)
+    {
+        if (!isClientScriptingEnabled())
+            return;
+
+        if (!(isRequired() || _minimum != null || _maximum != null))
+            return;
+
+        Map symbols = new HashMap();
+
+        if (isRequired())
+            symbols.put("requiredMessage", buildRequiredMessage(field));
+
+        if (isIntegerNumber())
+            symbols.put("formatMessage", buildInvalidIntegerFormatMessage(field));
+        else
+            symbols.put("formatMessage", buildInvalidNumericFormatMessage(field));
+
+        if (_minimum != null || _maximum != null)
+            symbols.put("rangeMessage", buildRangeMessage(field));
+
+        processValidatorScript(_scriptPath, cycle, field, symbols);
+    }
+
+    private String buildRangeMessage(IFormComponent field)
+    {
+        if (_minimum != null && _maximum != null)
+            return buildNumberRangeMessage(field);
+
+        if (_maximum != null)
+            return buildNumberTooLargeMessage(field);
+
+        return buildNumberTooSmallMessage(field);
+    }
+
+    /**
+     *  @since 2.2
+     * 
+     **/
+
+    public String getScriptPath()
+    {
+        return _scriptPath;
+    }
+
+    /**
+     *  Allows a developer to use the existing validation logic with a different client-side
+     *  script.  This is often sufficient to allow application-specific error presentation
+     *  (perhaps by using DHTML to update the content of a &lt;span&gt; tag, or to use
+     *  a more sophisticated pop-up window than <code>window.alert()</code>).
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public void setScriptPath(String scriptPath)
+    {
+        _scriptPath = scriptPath;
+    }
+
+    /** Sets the value type from a string type name.  The name may be
+     *  a scalar numeric type, a fully qualified class name, or the name
+     *  of a numeric wrapper type from java.lang (with the package name omitted).
+     * 
+     * @since 3.0 
+     * 
+     **/
+
+    public void setValueType(String typeName)
+    {
+        Class typeClass = (Class) TYPES.get(typeName);
+
+        if (typeClass == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.format("NumberValidator.unknown-type", typeName));
+
+        _valueTypeClass = typeClass;
+    }
+
+    /** @since 3.0 **/
+
+    public void setValueTypeClass(Class valueTypeClass)
+    {
+        _valueTypeClass = valueTypeClass;
+    }
+
+    /** 
+     *  
+     *  Returns the value type to convert strings back into.  The default is int.
+     * 
+     *  @since 3.0 
+     * 
+     **/
+
+    public Class getValueTypeClass()
+    {
+        return _valueTypeClass;
+    }
+
+    /** @since 3.0 */
+
+    public String getInvalidNumericFormatMessage()
+    {
+        return _invalidNumericFormatMessage;
+    }
+
+    /** @since 3.0 */
+
+    public String getInvalidIntegerFormatMessage()
+    {
+        return _invalidIntegerFormatMessage;
+    }
+
+    /** @since 3.0 */
+
+    public String getNumberRangeMessage()
+    {
+        return _numberRangeMessage;
+    }
+
+    /** @since 3.0 */
+
+    public String getNumberTooLargeMessage()
+    {
+        return _numberTooLargeMessage;
+    }
+
+    /** @since 3.0 */
+
+    public String getNumberTooSmallMessage()
+    {
+        return _numberTooSmallMessage;
+    }
+
+    /** 
+     * Overrides the <code>invalid-numeric-format</code> bundle key.
+     * Parameter {0} is the display name of the field.
+     * 
+     * @since 3.0
+     */
+
+    public void setInvalidNumericFormatMessage(String string)
+    {
+        _invalidNumericFormatMessage = string;
+    }
+
+    /** 
+     * Overrides the <code>invalid-int-format</code> bundle key.
+     * Parameter {0} is the display name of the field.
+     * 
+     * @since 3.0
+     */
+
+    public void setInvalidIntegerFormatMessage(String string)
+    {
+        _invalidIntegerFormatMessage = string;
+    }
+
+    /** @since 3.0 */
+
+    protected String buildInvalidNumericFormatMessage(IFormComponent field)
+    {
+        String pattern =
+            getPattern(
+                getInvalidNumericFormatMessage(),
+                "invalid-numeric-format",
+                field.getPage().getLocale());
+
+        return formatString(pattern, field.getDisplayName());
+    }
+
+    /** @since 3.0 */
+
+    protected String buildInvalidIntegerFormatMessage(IFormComponent field)
+    {
+        String pattern =
+            getPattern(
+                getInvalidIntegerFormatMessage(),
+                "invalid-int-format",
+                field.getPage().getLocale());
+
+        return formatString(pattern, field.getDisplayName());
+    }
+
+    /** 
+     * Overrides the <code>number-range</code> bundle key.
+     * Parameter [0} is the display name of the field.
+     * Parameter {1} is the minimum value.
+     * Parameter {2} is the maximum value.
+     * 
+     * @since 3.0
+     */
+
+    public void setNumberRangeMessage(String string)
+    {
+        _numberRangeMessage = string;
+    }
+
+    protected String buildNumberRangeMessage(IFormComponent field)
+    {
+        String pattern =
+            getPattern(_numberRangeMessage, "number-range", field.getPage().getLocale());
+
+        return formatString(pattern, new Object[] { field.getDisplayName(), _minimum, _maximum });
+    }
+
+    /**
+     *  Overrides the <code>number-too-large</code> bundle key.
+     *  Parameter {0} is the display name of the field.
+     *  Parameter {1} is the maximum allowed value.
+     *  
+     *  @since 3.0
+     */
+
+    public void setNumberTooLargeMessage(String string)
+    {
+        _numberTooLargeMessage = string;
+    }
+
+    /** @since 3.0 */
+
+    protected String buildNumberTooLargeMessage(IFormComponent field)
+    {
+        String pattern =
+            getPattern(_numberTooLargeMessage, "number-too-large", field.getPage().getLocale());
+
+        return formatString(pattern, field.getDisplayName(), _maximum);
+    }
+
+    /**
+     *  Overrides the <code>number-too-small</code> bundle key.
+     *  Parameter {0} is the display name of the field.
+     *  Parameter {1} is the minimum allowed value.
+     * 
+     *  @since 3.0
+     * 
+     */
+
+    public void setNumberTooSmallMessage(String string)
+    {
+        _numberTooSmallMessage = string;
+    }
+
+    /** @since 3.0 */
+
+    protected String buildNumberTooSmallMessage(IFormComponent field)
+    {
+        String pattern =
+            getPattern(_numberTooSmallMessage, "number-too-small", field.getPage().getLocale());
+
+        return formatString(pattern, field.getDisplayName(), _minimum);
+    }
+
+    /** @since 3.0 */
+
+    public boolean isIntegerNumber()
+    {
+        NumberAdaptor result = (NumberAdaptor) _numberAdaptors.getAdaptor(_valueTypeClass);
+        if (result == null)
+            return false;
+
+        return result.getNumberType() == NUMBER_TYPE_INTEGER;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/NumberValidator.script b/tapestry-framework/src/org/apache/tapestry/valid/NumberValidator.script
new file mode 100644
index 0000000..78924da
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/NumberValidator.script
@@ -0,0 +1,68 @@
+<?xml version="1.0"?>
+<!-- $Id$ -->
+<!DOCTYPE script PUBLIC
+	"-//Apache Software Foundation//Tapestry Script Specification 3.0//EN"
+	"http://jakarta.apache.org/tapestry/dtd/Script_3_0.dtd">
+	
+<!-- 
+
+  Creates a script for validating that a field is required and/or has a minimum
+  field length.
+  
+  Input symbols:
+     field, form, validator:  As normal for a validation script.
+     formatMessage: Message displayed if the input is not valid.
+	 requiredMessage: Message to display if the field is required yet blank.
+	 rangeMessage: Message to display if the field is not in the expected range. 
+     formatExpression: Regular expression for the field.
+-->
+	
+<script>
+
+<include-script resource-path="/org/apache/tapestry/valid/Validator.js"/>
+
+<input-symbol key="field" class="org.apache.tapestry.valid.ValidField" required="yes"/>
+<input-symbol key="form" class="org.apache.tapestry.IForm" required="yes"/>
+<input-symbol key="validator" class="org.apache.tapestry.valid.NumberValidator" required="yes"/>
+<input-symbol key="formatMessage" class="java.lang.String" required="yes"/>
+<input-symbol key="requiredMessage" class="java.lang.String"/>
+<input-symbol key="rangeMessage" class="java.lang.String"/>
+
+<let key="function" unique="yes">
+validate_${field.name}
+</let>
+
+<body>
+function ${function}()
+{
+  var field = document.${form.name}.${field.name}; 
+  var stringValue = field.value;  
+<if expression="validator.required">
+  if (stringValue.length == 0)
+     return validator_invalid_field(field, "${requiredMessage}");
+</if>
+<if-not expression="validator.required">
+  if (stringValue.length == 0)
+     return true;
+</if-not>
+  var value = stringValue * 1;
+  if (isNaN(value))
+      return validator_invalid_field(field, "${formatMessage}");
+<if expression="validator.integerNumber">
+  var regex = /\./;
+  if (stringValue.search(regex) != -1)
+      return validator_invalid_field(field, "${formatMessage}");
+</if>
+<if expression="validator.minimum != null">
+  if (value &lt; ${validator.minimum})
+     return validator_invalid_field(field, "${rangeMessage}");
+</if>
+<if expression="validator.maximum != null">
+  if (value &gt; ${validator.maximum})
+     return validator_invalid_field(field, "${rangeMessage}");
+</if>
+  return true;
+}
+</body>
+
+</script>
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/PatternDelegate.java b/tapestry-framework/src/org/apache/tapestry/valid/PatternDelegate.java
new file mode 100644
index 0000000..399eeff
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/PatternDelegate.java
@@ -0,0 +1,42 @@
+//  Copyright 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.tapestry.valid;
+
+/**
+ * Implementations of this interface will provide pattern utility services.
+ * 
+ * @author  Harish Krishnaswamy
+ * @version $Id$
+ * @since   3.0
+ */
+public interface PatternDelegate
+{
+    /**
+     * Answers the question whether the input string fulfills the pattern string provided.
+     * 
+     * @param patternString The pattern that the input string is compared against.
+     * @param input The string under test.
+     * @return Returns true if the pattern exists in the input string; returns false otherwise.
+     */
+    public boolean contains(String patternString, String input);
+    
+    /**
+     * Returns the escaped sequence of characters representing the pattern string provided.
+     * 
+     * @param patternString The raw sequence of characters that represent the pattern.
+     * @return The escaped sequence of characters that represent the pattern.
+     */
+    public String getEscapedPatternString(String patternString);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/PatternValidator.java b/tapestry-framework/src/org/apache/tapestry/valid/PatternValidator.java
new file mode 100644
index 0000000..275aaca
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/PatternValidator.java
@@ -0,0 +1,255 @@
+//  Copyright 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.tapestry.valid;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.form.IFormComponent;
+import org.apache.tapestry.util.RegexpMatcher;
+
+/**
+ * <p>The validator bean that provides a pattern validation service.
+ * 
+ * <p>The actual pattern matching algorithm is provided by the 
+ * {@link org.apache.tapestry.valid.PatternDelegate}. This enables the user to provide
+ * custom pattern matching implementations. In the event a custom implementation is not 
+ * provided, this validator will use the {@link org.apache.tapestry.util.RegexpMatcher}.
+ * 
+ * <p>This validator has the ability to provide client side validation on demand. 
+ * To enable client side validation simply set the <code>clientScriptingEnabled</code>
+ * property to <code>true</code>.
+ * The default implementation of the script will be in JavaScript and allows the user to 
+ * override this with a custom implementation by setting the path to the custom  
+ * script via {@link #setScriptPath(String)}.
+ * 
+ * @author  Harish Krishnaswamy
+ * @version $Id$
+ * @since   3.0
+ */
+public class PatternValidator extends BaseValidator
+{
+    /**
+     * The pattern that this validator will use to validate the input. The default 
+     * pattern is an empty string.
+     */
+    private String _patternString = "";
+
+    /**
+     * A custom message in the event of a validation failure.
+     */
+    private String _patternNotMatchedMessage;
+
+    /**
+     * The object that handles pattern matching.
+     */
+    private PatternDelegate _patternDelegate;
+
+    /**
+     * The location of the script specification for client side validation.
+     */
+    private String _scriptPath = "/org/apache/tapestry/valid/PatternValidator.script";
+
+    /**
+     * Returns custom validation failure message. The default message comes from 
+     * <code>ValidationStrings.properties</code> file for key 
+     * <code>pattern-not-matched</code>.
+     */
+    public String getPatternNotMatchedMessage()
+    {
+        return _patternNotMatchedMessage;
+    }
+
+    /**
+     * Returns the pattern that this validator uses for validation.
+     */
+    public String getPatternString()
+    {
+        return _patternString;
+    }
+
+    /**
+     * Allows for a custom message to be set typically via the bean specification.
+     */
+    public void setPatternNotMatchedMessage(String message)
+    {
+        _patternNotMatchedMessage = message;
+    }
+
+    /**
+     * Allows the user to change the validation pattern. 
+     */
+    public void setPatternString(String pattern)
+    {
+        _patternString = pattern;
+    }
+
+    /**
+     * Static inner class that acts as a delegate to RegexpMatcher and conforms to the 
+     * PatternDelegate contract.
+     */
+    private static class RegExpDelegate implements PatternDelegate
+    {
+        private RegexpMatcher _matcher;
+
+        private RegexpMatcher getPatternMatcher()
+        {
+            if (_matcher == null)
+                _matcher = new RegexpMatcher();
+
+            return _matcher;
+        }
+
+        public boolean contains(String patternString, String input)
+        {
+            return getPatternMatcher().contains(patternString, input);
+        }
+
+        public String getEscapedPatternString(String patternString)
+        {
+            return getPatternMatcher().getEscapedPatternString(patternString);
+        }
+    }
+
+    /**
+     * Allows for a custom implementation to do the pattern matching. The default pattern 
+     * matching is done with {@link org.apache.tapestry.util.RegexpMatcher}.
+     */
+    public void setPatternDelegate(PatternDelegate patternDelegate)
+    {
+        _patternDelegate = patternDelegate;
+    }
+
+    /**
+     * Returns the custom pattern matcher if one is provided or creates and returns the 
+     * default matcher laziliy.
+     */
+    public PatternDelegate getPatternDelegate()
+    {
+        if (_patternDelegate == null)
+            _patternDelegate = new RegExpDelegate();
+
+        return _patternDelegate;
+    }
+
+    /**
+     * @see org.apache.tapestry.valid.IValidator#toString(org.apache.tapestry.form.IFormComponent, java.lang.Object)
+     */
+    public String toString(IFormComponent field, Object value)
+    {
+        if (value == null)
+            return null;
+
+        return value.toString();
+    }
+
+    private String buildPatternNotMatchedMessage(IFormComponent field, String patternString)
+    {
+        String templateMessage =
+            getPattern(
+                _patternNotMatchedMessage,
+                "pattern-not-matched",
+                field.getPage().getLocale());
+
+        return formatString(templateMessage, field.getDisplayName(), patternString);
+    }
+
+    /**
+     * @see org.apache.tapestry.valid.IValidator#toObject(org.apache.tapestry.form.IFormComponent, java.lang.String)
+     */
+    public Object toObject(IFormComponent field, String input) throws ValidatorException
+    {
+        if (checkRequired(field, input))
+            return null;
+
+        boolean matched = false;
+
+        try
+        {
+            matched = getPatternDelegate().contains(_patternString, input);
+        }
+        catch (Throwable t)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "PatternValidator.pattern-match-error",
+                    _patternString,
+                    field.getDisplayName()),
+                field,
+                field.getLocation(),
+                t);
+        }
+
+        if (!matched)
+            throw new ValidatorException(
+                buildPatternNotMatchedMessage(field, _patternString),
+                ValidationConstraint.PATTERN_MISMATCH);
+
+        return input;
+    }
+
+    /**
+     * Allows for a custom implementation of the client side validation.
+     */
+    public void setScriptPath(String scriptPath)
+    {
+        _scriptPath = scriptPath;
+    }
+
+    /**
+     * @see org.apache.tapestry.valid.IValidator#renderValidatorContribution(org.apache.tapestry.form.IFormComponent, org.apache.tapestry.IMarkupWriter, org.apache.tapestry.IRequestCycle)
+     */
+    public void renderValidatorContribution(
+        IFormComponent field,
+        IMarkupWriter writer,
+        IRequestCycle cycle)
+    {
+        if (!isClientScriptingEnabled())
+            return;
+
+        Map symbols = new HashMap();
+
+        if (isRequired())
+            symbols.put("requiredMessage", buildRequiredMessage(field));
+
+        symbols.put(
+            "patternNotMatchedMessage",
+            buildPatternNotMatchedMessage(field, getEscapedPatternString()));
+
+        processValidatorScript(_scriptPath, cycle, field, symbols);
+    }
+
+    /**
+     * Returns the escaped sequence of the pattern string for rendering in the error message. 
+     */
+    public String getEscapedPatternString()
+    {
+        return getPatternDelegate().getEscapedPatternString(_patternString);
+    }
+
+    public String toString()
+    {
+        return "Pattern: "
+            + _patternString
+            + "; Script Path: "
+            + _scriptPath
+            + "; Pattern Delegate: "
+            + _patternDelegate;
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/PatternValidator.script b/tapestry-framework/src/org/apache/tapestry/valid/PatternValidator.script
new file mode 100644
index 0000000..fdaef23
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/PatternValidator.script
@@ -0,0 +1,52 @@
+<?xml version="1.0"?>

+<!-- $Id: $ -->

+<!DOCTYPE script PUBLIC

+	"-//Apache Software Foundation//Tapestry Script Specification 3.0//EN"

+	"http://jakarta.apache.org/tapestry/dtd/Script_3_0.dtd">

+<!-- 

+

+  Creates a script for validating that a field matches a required pattern.

+  

+  Input symbols:

+     field, form, validator:  As normal for a validation script.

+	 requiredMessage: Message to display if the field is required yet blank.

+	 patternNotMatchedMessage: Message to display if the field does not fulfill the required pattern.

+     

+-->

+	

+<script>

+

+<include-script resource-path="/org/apache/tapestry/valid/Validator.js"/>

+

+<input-symbol key="field" class="org.apache.tapestry.valid.ValidField" required="yes"/>

+<input-symbol key="form" class="org.apache.tapestry.IForm" required="yes"/>

+<input-symbol key="validator" class="org.apache.tapestry.valid.PatternValidator" required="yes"/>

+<input-symbol key="requiredMessage" class="java.lang.String"/>

+<input-symbol key="patternNotMatchedMessage" class="java.lang.String" required="yes"/>

+

+<let key="function" unique="yes">

+    validate_${field.name}

+</let>

+

+<let key="pattern" unique="yes">

+    pattern_${field.name}

+</let>

+

+<body>

+var ${pattern} = new RegExp("${validator.escapedPatternString}");

+

+function ${function}()

+{

+    var field = document.${form.name}.${field.name};

+<if expression="validator.required">

+    if (field.value.length == 0)

+        return validator_invalid_field(field, "${requiredMessage}");

+</if>

+    if (field.value.length &gt; 0 &amp;&amp; !${pattern}.test(field.value))

+        return validator_invalid_field(field, "${patternNotMatchedMessage}");

+

+    return true;

+}

+</body>

+

+</script>
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/RenderString.java b/tapestry-framework/src/org/apache/tapestry/valid/RenderString.java
new file mode 100644
index 0000000..402aa63
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/RenderString.java
@@ -0,0 +1,83 @@
+//  Copyright 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.tapestry.valid;
+
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ * A wrapper around {@link String}that allows the String to be renderred. This is primarily used to
+ * present error messages.
+ * 
+ * @author Howard Lewis Ship
+ */
+
+public class RenderString implements IRender
+{
+    private String _string;
+
+    private boolean _raw = false;
+
+    public RenderString(String string)
+    {
+        _string = string;
+    }
+
+    /**
+     * @param string
+     *            the string to render
+     * @param raw
+     *            if true, the String is rendered as-is, with no filtering. If false (the default),
+     *            the String is filtered.
+     */
+
+    public RenderString(String string, boolean raw)
+    {
+        _string = string;
+        _raw = raw;
+    }
+
+    /**
+     * Renders the String to the writer. Does nothing if the string is null. If raw is true, uses
+     * {@link IMarkupWriter#printRaw(String)}, otherwise {@link IMarkupWriter#print(String)}.
+     */
+
+    public void render(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        if (_string == null)
+            return;
+
+        if (_raw)
+            writer.printRaw(_string);
+        else
+            writer.print(_string);
+    }
+
+    public String getString()
+    {
+        return _string;
+    }
+
+    public boolean isRaw()
+    {
+        return _raw;
+    }
+
+    public String toString()
+    {
+        return _string;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/StringValidator.java b/tapestry-framework/src/org/apache/tapestry/valid/StringValidator.java
new file mode 100644
index 0000000..a61bdb5
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/StringValidator.java
@@ -0,0 +1,168 @@
+//  Copyright 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.tapestry.valid;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.form.IFormComponent;
+
+/**
+ *  Simple validation of strings, to enforce required, and minimum length
+ *  (maximum length is enforced in the client browser, by setting a maximum input
+ *  length on the text field).
+ *
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.8
+ *
+ **/
+
+public class StringValidator extends BaseValidator
+{
+    private int _minimumLength;
+
+    private String _minimumLengthMessage;
+
+    /** @since 2.2 **/
+
+    private String _scriptPath = "/org/apache/tapestry/valid/StringValidator.script";
+
+    public StringValidator()
+    {
+    }
+
+    private StringValidator(boolean required)
+    {
+        super(required);
+    }
+
+    public String toString(IFormComponent field, Object value)
+    {
+        if (value == null)
+            return null;
+
+        return value.toString();
+    }
+
+    public Object toObject(IFormComponent field, String input) throws ValidatorException
+    {
+        if (checkRequired(field, input))
+            return null;
+
+        if (_minimumLength > 0 && input.length() < _minimumLength)
+            throw new ValidatorException(
+                buildMinimumLengthMessage(field),
+                ValidationConstraint.MINIMUM_WIDTH);
+
+        return input;
+    }
+
+    public int getMinimumLength()
+    {
+        return _minimumLength;
+    }
+
+    public void setMinimumLength(int minimumLength)
+    {
+        _minimumLength = minimumLength;
+    }
+
+    /** 
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public void renderValidatorContribution(
+        IFormComponent field,
+        IMarkupWriter writer,
+        IRequestCycle cycle)
+    {
+        if (!isClientScriptingEnabled())
+            return;
+
+        if (!(isRequired() || _minimumLength > 0))
+            return;
+
+        Map symbols = new HashMap();
+
+        if (isRequired())
+            symbols.put("requiredMessage", buildRequiredMessage(field));
+
+        if (_minimumLength > 0)
+            symbols.put("minimumLengthMessage", buildMinimumLengthMessage(field));
+
+        processValidatorScript(_scriptPath, cycle, field, symbols);
+    }
+
+    /**
+     *  @since 2.2
+     * 
+     **/
+
+    public String getScriptPath()
+    {
+        return _scriptPath;
+    }
+
+    /**
+     *  Allows a developer to use the existing validation logic with a different client-side
+     *  script.  This is often sufficient to allow application-specific error presentation
+     *  (perhaps by using DHTML to update the content of a &lt;span&gt; tag, or to use
+     *  a more sophisticated pop-up window than <code>window.alert()</code>).
+     * 
+     *  @since 2.2
+     * 
+     **/
+
+    public void setScriptPath(String scriptPath)
+    {
+        _scriptPath = scriptPath;
+    }
+
+    /** @since 3.0 */
+    public String getMinimumLengthMessage()
+    {
+        return _minimumLengthMessage;
+    }
+
+    /** 
+     * Overrides the <code>field-too-short</code> bundle key.
+     * Parameter {0} is the minimum length.
+     * Parameter {1} is the display name of the field.
+     * 
+     * @since 3.0
+     */
+
+    public void setMinimumLengthMessage(String string)
+    {
+        _minimumLengthMessage = string;
+    }
+
+    /** @since 3.0 */
+
+    protected String buildMinimumLengthMessage(IFormComponent field)
+    {
+        String pattern =
+            getPattern(_minimumLengthMessage, "field-too-short", field.getPage().getLocale());
+
+        return formatString(pattern, Integer.toString(_minimumLength), field.getDisplayName());
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/StringValidator.script b/tapestry-framework/src/org/apache/tapestry/valid/StringValidator.script
new file mode 100644
index 0000000..ff508ed
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/StringValidator.script
@@ -0,0 +1,54 @@
+<?xml version="1.0"?>
+<!-- $Id$ -->
+<!DOCTYPE script PUBLIC
+	"-//Apache Software Foundation//Tapestry Script Specification 3.0//EN"
+	"http://jakarta.apache.org/tapestry/dtd/Script_3_0.dtd">
+<!--
+
+  Creates a script for validating that a field is required and/or has a minimum
+  field length.
+
+  Input symbols:
+     field, form, validator:  As normal for a validation script.
+	 requiredMessage: Message to display if the field is required yet blank.
+	 minimumLengthMessage: Message to display if the field length is too short.
+
+-->
+
+<script>
+
+<include-script resource-path="/org/apache/tapestry/valid/Validator.js"/>
+
+<input-symbol key="field" class="org.apache.tapestry.valid.ValidField" required="yes"/>
+<input-symbol key="form" class="org.apache.tapestry.IForm" required="yes"/>
+<input-symbol key="validator" class="org.apache.tapestry.valid.StringValidator" required="yes"/>
+<input-symbol key="requiredMessage" class="java.lang.String"/>
+<input-symbol key="minimumLengthMessage" class="java.lang.String"/>
+
+<let key="function" unique="yes">
+validate_${field.name}
+</let>
+
+<body>
+function ${function}()
+{
+  var field = document.${form.name}.${field.name};
+<if expression="validator.required">
+  if (field.value.length == 0)
+     return validator_invalid_field(field, "${requiredMessage}");
+</if>
+
+<if-not expression="validator.required">
+  if (field.value.length == 0)
+     return true;
+</if-not>
+
+<if expression="validator.minimumLength">
+  if (field.value.length &lt; ${validator.minimumLength})
+     return validator_invalid_field(field, "${minimumLengthMessage}");
+</if>
+  return true;
+}
+</body>
+
+</script>
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/UrlValidator.java b/tapestry-framework/src/org/apache/tapestry/valid/UrlValidator.java
new file mode 100644
index 0000000..280a900
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/UrlValidator.java
@@ -0,0 +1,276 @@
+//  Copyright 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.tapestry.valid;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.Vector;
+
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.form.IFormComponent;
+import org.apache.tapestry.util.StringSplitter;
+
+/**
+ *
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+public class UrlValidator extends BaseValidator {
+	private int _minimumLength;
+	private String _minimumLengthMessage;
+	private String _invalidUrlFormatMessage;
+	private String _disallowedProtocolMessage;
+	private Collection _allowedProtocols;
+
+	private String _scriptPath = "/org/apache/tapestry/valid/UrlValidator.script"; //$NON-NLS-1$
+
+	public UrlValidator() {
+	}
+
+	private UrlValidator(boolean required) {
+		super(required);
+	}
+
+	public String toString(IFormComponent field, Object value) {
+		if (value == null)
+			return null;
+
+		return value.toString();
+	}
+
+	public Object toObject(IFormComponent field, String input)
+		throws ValidatorException {
+		if (checkRequired(field, input))
+			return null;
+
+		if (_minimumLength > 0 && input.length() < _minimumLength)
+			throw new ValidatorException(
+				buildMinimumLengthMessage(field),
+				ValidationConstraint.MINIMUM_WIDTH);
+
+		if (!isValidUrl(input))
+			throw new ValidatorException(
+				buildInvalidUrlFormatMessage(field),
+				ValidationConstraint.URL_FORMAT);
+
+		if (!isAllowedProtocol(input)) {
+			throw new ValidatorException(
+				buildDisallowedProtocolMessage(field),
+				ValidationConstraint.DISALLOWED_PROTOCOL);
+		}
+
+		return input;
+	}
+
+	public int getMinimumLength() {
+		return _minimumLength;
+	}
+
+	public void setMinimumLength(int minimumLength) {
+		_minimumLength = minimumLength;
+	}
+
+	public void renderValidatorContribution(
+		IFormComponent field,
+		IMarkupWriter writer,
+		IRequestCycle cycle) {
+		if (!isClientScriptingEnabled())
+			return;
+
+		Map symbols = new HashMap();
+
+		if (isRequired())
+			symbols.put("requiredMessage", buildRequiredMessage(field)); //$NON-NLS-1$
+
+		if (_minimumLength > 0)
+			symbols.put("minimumLengthMessage", //$NON-NLS-1$
+			buildMinimumLengthMessage(field));
+
+		symbols.put("urlFormatMessage", buildInvalidUrlFormatMessage(field)); //$NON-NLS-1$
+
+		symbols.put("urlDisallowedProtocolMessage", //$NON-NLS-1$
+		buildDisallowedProtocolMessage(field));
+
+		symbols.put("urlRegexpProtocols", buildUrlRegexpProtocols()); //$NON-NLS-1$
+
+		processValidatorScript(_scriptPath, cycle, field, symbols);
+	}
+
+	private String buildUrlRegexpProtocols() {
+		if(_allowedProtocols == null) {
+			return null;
+		}
+		String regexp = "/("; //$NON-NLS-1$
+		Iterator iter = _allowedProtocols.iterator();
+		while (iter.hasNext()) {
+			String protocol = (String) iter.next();
+			regexp += protocol;
+			if (iter.hasNext()) {
+				regexp += "|"; //$NON-NLS-1$
+			}
+		}
+		regexp += "):///"; //$NON-NLS-1$
+		return regexp;
+	}
+
+	public String getScriptPath() {
+		return _scriptPath;
+	}
+
+	public void setScriptPath(String scriptPath) {
+		_scriptPath = scriptPath;
+	}
+
+	protected boolean isValidUrl(String url) {
+		boolean bIsValid;
+		try {
+			new URL(url);
+			bIsValid = true;
+		} catch (MalformedURLException mue) {
+			bIsValid = false;
+		}
+		return bIsValid;
+	}
+
+	protected boolean isAllowedProtocol(String url) {
+		boolean bIsAllowed = false;
+		if (_allowedProtocols != null) {
+			URL oUrl;
+			try {
+				oUrl = new URL(url);
+			} catch (MalformedURLException e) {
+				return false;
+			}
+			String actualProtocol = oUrl.getProtocol();
+			Iterator iter = _allowedProtocols.iterator();
+			while (iter.hasNext()) {
+				String protocol = (String) iter.next();
+				if (protocol.equals(actualProtocol)) {
+					bIsAllowed = true;
+					break;
+				}
+			}
+		} else {
+			bIsAllowed = true;
+		}
+		return bIsAllowed;
+	}
+
+	public String getInvalidUrlFormatMessage() {
+		return _invalidUrlFormatMessage;
+	}
+
+	public String getMinimumLengthMessage() {
+		return _minimumLengthMessage;
+	}
+
+	public void setInvalidUrlFormatMessage(String string) {
+		_invalidUrlFormatMessage = string;
+	}
+
+	public String getDisallowedProtocolMessage() {
+		return _disallowedProtocolMessage;
+	}
+
+	public void setDisallowedProtocolMessage(String string) {
+		_disallowedProtocolMessage = string;
+	}
+
+	public void setMinimumLengthMessage(String string) {
+		_minimumLengthMessage = string;
+	}
+
+	protected String buildMinimumLengthMessage(IFormComponent field) {
+			String pattern = getPattern(_minimumLengthMessage, "field-too-short", //$NON-NLS-1$
+	field.getPage().getLocale());
+
+		return formatString(
+			pattern,
+			Integer.toString(_minimumLength),
+			field.getDisplayName());
+	}
+
+	protected String buildInvalidUrlFormatMessage(IFormComponent field) {
+			String pattern = getPattern(_invalidUrlFormatMessage, "invalid-url-format", //$NON-NLS-1$
+	field.getPage().getLocale());
+
+		return formatString(pattern, field.getDisplayName());
+	}
+
+	protected String buildDisallowedProtocolMessage(IFormComponent field) {
+		if(_allowedProtocols == null) {
+			return null;
+		}
+			String pattern = getPattern(_disallowedProtocolMessage, "disallowed-protocol", //$NON-NLS-1$
+	field.getPage().getLocale());
+
+		String allowedProtocols = ""; //$NON-NLS-1$
+		Iterator iter = _allowedProtocols.iterator();
+		while (iter.hasNext()) {
+			String protocol = (String) iter.next();
+			if (!allowedProtocols.equals("")) { //$NON-NLS-1$
+				if(iter.hasNext()) {
+					allowedProtocols += ", "; //$NON-NLS-1$
+				} else {
+					allowedProtocols += " or "; //$NON-NLS-1$
+				}
+			}
+			allowedProtocols += protocol;			
+		}
+
+		return formatString(pattern, allowedProtocols);
+	}
+
+	protected String getPattern(String override, String key, Locale locale) {
+		if (override != null)
+			return override;
+
+		ResourceBundle strings;
+		String string;
+		try {
+				strings = ResourceBundle.getBundle("net.sf.cendil.tapestry.valid.ValidationStrings", //$NON-NLS-1$
+	locale);
+			string = strings.getString(key);
+		} catch (Exception exc) {
+				strings = ResourceBundle.getBundle("org.apache.tapestry.valid.ValidationStrings", //$NON-NLS-1$
+	locale);
+			string = strings.getString(key);
+		}
+
+		return string;
+	}
+
+	/**
+	 * @param protocols comma separated list of allowed protocols
+	 */
+	public void setAllowedProtocols(String protocols) {
+		StringSplitter spliter = new StringSplitter(',');
+		//String[] aProtocols = protocols.split(","); //$NON-NLS-1$
+		String[] aProtocols = spliter.splitToArray(protocols); //$NON-NLS-1$
+		_allowedProtocols = new Vector();
+		for (int i = 0; i < aProtocols.length; i++) {
+			_allowedProtocols.add(aProtocols[i]);
+		}
+	}
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/UrlValidator.script b/tapestry-framework/src/org/apache/tapestry/valid/UrlValidator.script
new file mode 100644
index 0000000..1795d3d
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/UrlValidator.script
@@ -0,0 +1,70 @@
+<?xml version="1.0"?>
+<!DOCTYPE script
+	PUBLIC "-//Apache Software Foundation//Tapestry Script Specification 3.0//EN"
+	"http://jakarta.apache.org/tapestry/dtd/Script_3_0.dtd"
+>
+
+
+<!--
+	Creates a script for validating that a field is required and/or has a minimum
+	field length.
+
+	Input symbols:
+	field, form, validator:  As normal for a validation script.
+	requiredMessage: Message to display if the field is required yet blank.
+	minimumLengthMessage: Message to display if the field length is too short.
+	urlFormatMessage: Message to display if the field value is not a valid URL.
+	urlRegexpProtocols: The regexp to check that the protocol is one of the allowed protocols.
+	urlDisallowedProtocolMessage: Message to display if the field value does not use an allowed protocol.
+
+-->
+<script>
+
+	<include-script resource-path="/org/apache/tapestry/valid/Validator.js" />
+
+	<input-symbol key="field" class="org.apache.tapestry.valid.ValidField" required="yes" />
+	<input-symbol key="form" class="org.apache.tapestry.IForm" required="yes" />
+	<input-symbol key="validator" class="org.apache.tapestry.valid.UrlValidator" required="yes" />
+	<input-symbol key="requiredMessage" class="java.lang.String" />
+	<input-symbol key="minimumLengthMessage" class="java.lang.String" />
+	<input-symbol key="urlFormatMessage" class="java.lang.String" />
+	<input-symbol key="urlRegexpProtocols" class="java.lang.String" />
+	<input-symbol key="urlDisallowedProtocolMessage" class="java.lang.String" />
+
+	<let key="function" unique="yes">validate_${field.name}</let>
+
+	<body>
+		function ${function}() {
+			var field = document.${form.name}.${field.name};
+			strValue = field.value.replace(/ /g,"");
+			field.value = strValue;
+
+		<if expression="validator.required">
+			if (strValue.length == 0)
+				return validator_invalid_field(field, "${requiredMessage}");
+		</if>
+
+		<if-not expression="validator.required">
+			if (strValue.length == 0)
+				return true;
+		</if-not>
+
+		<if expression="validator.minimumLength">
+			if (strValue.length &lt; ${validator.minimumLength})
+				return validator_invalid_field(field, "${minimumLengthMessage}");
+		</if>
+
+			if(!regexpTestUrl(strValue))
+				return validator_invalid_field(field, "${urlFormatMessage}");
+
+		<if expression="null != urlRegexpProtocols">
+			var protoRegExp = ${urlRegexpProtocols};
+			if(!protoRegExp.test(strValue))
+				return validator_invalid_field(field, "${urlDisallowedProtocolMessage}");
+		</if>
+
+			return true;
+		}
+	</body>
+
+</script>
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/ValidField.java b/tapestry-framework/src/org/apache/tapestry/valid/ValidField.java
new file mode 100644
index 0000000..ea85b64
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/ValidField.java
@@ -0,0 +1,197 @@
+//  Copyright 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.tapestry.valid;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.form.AbstractTextField;
+import org.apache.tapestry.form.Form;
+import org.apache.tapestry.form.IFormComponent;
+import org.apache.tapestry.html.Body;
+
+/**
+ *
+ *  A {@link Form} component that creates a text field that
+ *  allows for validation of user input and conversion between string and object
+ *  values. 
+ * 
+ *  [<a href="../../../../../ComponentReference/ValidField.html">Component Reference</a>]
+ * 
+ *  <p> A ValidatingTextField uses an {@link IValidationDelegate} to 
+ *  track errors and an {@link IValidator} to convert between strings and objects
+ *  (as well as perform validations).  The validation delegate is shared by all validating
+ *  text fields in a form, the validator may be shared my multiple elements as desired.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *
+ **/
+
+public abstract class ValidField extends AbstractTextField implements IFormComponent
+{
+    public abstract Object getValue();
+    public abstract void setValue(Object value);
+
+    public abstract String getDisplayName();
+
+    /**
+     *
+     *  Renders the component, which involves the {@link IValidationDelegate delegate}.
+     *
+     *  <p>During a render, the <em>first</em> field rendered that is either
+     *  in error, or required but null gets special treatment.  JavaScript is added
+     *  to select that field (such that the cursor jumps right to the field when the
+     *  page loads).
+     *
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        IForm form = getForm(cycle);
+        IValidationDelegate delegate = form.getDelegate();
+
+        if (delegate == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.format(
+                    "ValidField.no-delegate",
+                    getExtendedId(),
+                    getForm().getExtendedId()),
+                this,
+                null,
+                null);
+
+        IValidator validator = getValidator();
+
+        if (validator == null)
+            throw Tapestry.createRequiredParameterException(this, "validator");
+
+        boolean rendering = !cycle.isRewinding();
+
+        if (rendering)
+            delegate.writePrefix(writer, cycle, this, validator);
+
+        super.renderComponent(writer, cycle);
+
+        if (rendering)
+            delegate.writeSuffix(writer, cycle, this, validator);
+
+        // If rendering and there's either an error in the field,
+        // then we may have identified the default field (which will
+        // automatically receive focus).
+
+        if (rendering && delegate.isInError())
+            addSelect(cycle);
+
+        // That's OK, but an ideal situation would know about non-validating
+        // text fields, and also be able to put the cursor in the
+        // first field, period (even if there are no required or error fields).
+        // Still, this pretty much rocks!
+
+    }
+
+    /**
+     *  Invokes {@link IValidationDelegate#writeAttributes(IMarkupWriter,IRequestCycle, IFormComponent,IValidator)}.
+     *
+     **/
+
+    protected void beforeCloseTag(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        IValidator validator = getValidator();
+
+        validator.renderValidatorContribution(this, writer, cycle);
+
+        getForm().getDelegate().writeAttributes(writer, cycle, this, validator);
+    }
+
+    private static final String SELECTED_ATTRIBUTE_NAME =
+        "org.apache.tapestry.component.html.valid.SelectedFieldSet";
+
+    /**
+     *  Creates JavaScript to set the cursor on the first required or error
+     *  field encountered while rendering.  This only works if the text field
+     *  is wrapped by a {@link Body} component (which is almost always true).
+     *
+     **/
+
+    protected void addSelect(IRequestCycle cycle)
+    {
+        // If some other field has taken the honors, then let it.
+
+        if (cycle.getAttribute(SELECTED_ATTRIBUTE_NAME) != null)
+            return;
+
+        Body body = Body.get(cycle);
+
+        // If not wrapped by a Body, then do nothing.
+
+        if (body == null)
+            return;
+
+        IForm form = Form.get(cycle);
+
+        String formName = form.getName();
+        String textFieldName = getName();
+
+        String fullName = "document." + formName + "." + textFieldName;
+
+        body.addInitializationScript(fullName + ".focus();");
+        body.addInitializationScript(fullName + ".select();");
+
+        // Put a marker in, indicating that the selected field is known.
+
+        cycle.setAttribute(SELECTED_ATTRIBUTE_NAME, Boolean.TRUE);
+    }
+
+    protected String readValue()
+    {
+        IValidationDelegate delegate = getForm().getDelegate();
+
+        if (delegate.isInError())
+            return delegate.getFieldInputValue();
+
+        Object value = getValue();
+        String result = getValidator().toString(this, value);
+
+        if (Tapestry.isBlank(result) && getValidator().isRequired())
+            addSelect(getPage().getRequestCycle());
+
+        return result;
+    }
+
+    protected void updateValue(String value)
+    {
+        Object objectValue = null;
+        IValidationDelegate delegate = getForm().getDelegate();
+
+        delegate.recordFieldInputValue(value);
+
+        try
+        {
+            objectValue = getValidator().toObject(this, value);
+        }
+        catch (ValidatorException ex)
+        {
+            delegate.record(ex);
+            return;
+        }
+
+        setValue(objectValue);
+    }
+
+    public abstract IValidator getValidator();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/ValidField.jwc b/tapestry-framework/src/org/apache/tapestry/valid/ValidField.jwc
new file mode 100644
index 0000000..0011476
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/ValidField.jwc
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<component-specification class="org.apache.tapestry.valid.ValidField" allow-body="no">
+
+  <description>
+  A text input field that can validate input.
+  </description>
+
+  <parameter name="value" type="java.lang.Object" required="yes" direction="auto"/>
+  
+  <parameter name="disabled" type="boolean" direction="in"/>
+  
+  <parameter name="hidden" type="boolean" direction="in"/>
+    
+  <parameter name="validator" type="org.apache.tapestry.valid.IValidator" required="yes" direction="auto">
+    <description>
+    Converts value to a string and parses strings back into object values.
+    </description>
+  </parameter>
+    
+  <parameter name="displayName" type="java.lang.String" required="yes" direction="auto">
+    <description>
+    Name used by FieldLabel and when generating validation error messages.
+    </description>
+  </parameter>
+  
+  <reserved-parameter name="type"/>
+  <reserved-parameter name="value"/>
+  <reserved-parameter name="name"/>
+
+  <property-specification name="name" type="java.lang.String"/>
+  <property-specification name="form" type="org.apache.tapestry.IForm"/>
+  
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/ValidationConstraint.java b/tapestry-framework/src/org/apache/tapestry/valid/ValidationConstraint.java
new file mode 100644
index 0000000..ddc0146
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/ValidationConstraint.java
@@ -0,0 +1,134 @@
+//  Copyright 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.tapestry.valid;
+
+import org.apache.commons.lang.enum.Enum;
+
+/**
+ *  Defines an enumeration of different types of validation constraints
+ *  that may be violated.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ * 
+ **/
+
+public class ValidationConstraint extends Enum
+{
+    /**
+     *  Indicates that no value (or a value consisting only of white space) was
+     *  provided for a field that requires a non-null value.
+     *
+     **/
+
+    public static final ValidationConstraint REQUIRED = new ValidationConstraint("REQUIRED");
+
+    /**
+     *  Indicates that a non-null value was provided, but that (after removing
+     *  leading and trailing whitespace), the value was not long enough.
+     *
+     **/
+
+    public static final ValidationConstraint MINIMUM_WIDTH =
+        new ValidationConstraint("MINUMUM_WIDTH");
+
+    /**
+     *  Indicates a general error in converting a String into a Date.
+     *
+     **/
+
+    public static final ValidationConstraint DATE_FORMAT = new ValidationConstraint("DATE_FORMAT");
+
+    /**
+     *  Indicates a general error in the format of a string that is
+     *  to be interpreted as a email.
+     *
+     **/
+
+    public static final ValidationConstraint EMAIL_FORMAT =
+        new ValidationConstraint("EMAIL_FORMAT");
+
+    /**
+     *  Indicates a general error in the format of a string that is
+     *  to be interpreted as a number.
+     *
+     **/
+
+    public static final ValidationConstraint NUMBER_FORMAT =
+        new ValidationConstraint("NUMBER_FORMAT");
+
+    /**
+     *  Indicates that the value was too small (for a Date, too early).
+     *
+     **/
+
+    public static final ValidationConstraint TOO_SMALL = new ValidationConstraint("TOO_SMALL");
+
+    /**
+     *  Indicates that the value was too large (for a Date, too late).
+     *
+     **/
+
+    public static final ValidationConstraint TOO_LARGE = new ValidationConstraint("TOO_LARGE");
+
+    /**
+     *  Indicates an error in a string that does not fulfill a pattern.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    public static final ValidationConstraint PATTERN_MISMATCH =
+        new ValidationConstraint("PATTERN_MISMATCH");
+
+    /**
+     *  Indicates a consistency error, usually between too different fields.
+     * 
+     *  @since 3.0
+     * 
+     **/
+
+    public static final ValidationConstraint CONSISTENCY = new ValidationConstraint("CONSISTENCY");
+
+	/**
+	 *  Indicates that a URL is not of the correct format
+	 * 
+	 * @since 3.0
+	 */
+	
+	public static final ValidationConstraint URL_FORMAT = new ValidationConstraint("URL_FORMAT");
+
+	/**
+	 *  Indicates that the URL does not use one of the specified protocols
+	 * 
+	 * @since 3.0
+	 */
+
+	public static final ValidationConstraint DISALLOWED_PROTOCOL = new ValidationConstraint("DISALLOWED_PROTOCOL");
+
+
+
+	/**
+	 *  Protected constructor, which allows new constraints to be created
+	 *  as subclasses.
+	 * 
+	 **/
+
+    protected ValidationConstraint(String name)
+    {
+        super(name);
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/ValidationDelegate.java b/tapestry-framework/src/org/apache/tapestry/valid/ValidationDelegate.java
new file mode 100644
index 0000000..6b3c03e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/ValidationDelegate.java
@@ -0,0 +1,460 @@
+//  Copyright 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.tapestry.valid;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRender;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.form.IFormComponent;
+
+/**
+ * A base implementation of {@link IValidationDelegate}that can be used as a helper bean. This
+ * class is often subclassed, typically to override presentation details.
+ * 
+ * @author Howard Lewis Ship
+ * @since 1.0.5
+ */
+
+public class ValidationDelegate implements IValidationDelegate
+{
+    private IFormComponent _currentComponent;
+
+    /**
+     * List of {@link FieldTracking}.
+     */
+    private List _trackings;
+
+    /**
+     * A Map of Maps, keyed on the name of the Form. Each inner map contains the trackings for one
+     * form, keyed on component name. Care must be taken, because the inner Map is not always
+     * present.
+     * <p>
+     * Each ultimate {@link FieldTracking}object is also in the _trackings list.
+     */
+
+    private Map _trackingMap;
+
+    public void clear()
+    {
+        _currentComponent = null;
+        _trackings = null;
+        _trackingMap = null;
+    }
+
+    public void clearErrors()
+    {
+        if (_trackings == null)
+            return;
+
+        Iterator i = (Iterator) _trackings.iterator();
+        while (i.hasNext())
+        {
+            FieldTracking ft = (FieldTracking) i.next();
+            ft.setErrorRenderer(null);
+        }
+    }
+
+    /**
+     * If the form component is in error, places a &lt;font color="red"&lt; around it. Note: this
+     * will only work on the render phase after a rewind, and will be confused if components are
+     * inside any kind of loop.
+     */
+
+    public void writeLabelPrefix(IFormComponent component, IMarkupWriter writer, IRequestCycle cycle)
+    {
+        if (isInError(component))
+        {
+            writer.begin("font");
+            writer.attribute("color", "red");
+        }
+    }
+
+    /**
+     * Closes the &lt;font&gt; element,started by
+     * {@link #writeLabelPrefix(IFormComponent,IMarkupWriter,IRequestCycle)}, if the form component
+     * is in error.
+     */
+
+    public void writeLabelSuffix(IFormComponent component, IMarkupWriter writer, IRequestCycle cycle)
+    {
+        if (isInError(component))
+        {
+            writer.end();
+        }
+    }
+
+    /**
+     * Returns the {@link IFieldTracking}for the current component, if any. The
+     * {@link IFieldTracking}is usually created in {@link #record(String, ValidationConstraint)}or
+     * in {@link #record(IRender, ValidationConstraint)}.
+     * <p>
+     * Components may be rendered multiple times, with multiple names (provided by the
+     * {@link org.apache.tapestry.form.Form}, care must be taken that this method is invoked
+     * <em>after</em> the Form has provided a unique {@link IFormComponent#getName()}for the
+     * component.
+     * 
+     * @see #setFormComponent(IFormComponent)
+     * @return the {@link FieldTracking}, or null if the field has no tracking.
+     */
+
+    protected FieldTracking getComponentTracking()
+    {
+        if (_trackingMap == null)
+            return null;
+
+        String formName = _currentComponent.getForm().getName();
+
+        Map formMap = (Map) _trackingMap.get(formName);
+
+        if (formMap == null)
+            return null;
+
+        return (FieldTracking) formMap.get(_currentComponent.getName());
+    }
+
+    public void setFormComponent(IFormComponent component)
+    {
+        _currentComponent = component;
+    }
+
+    public boolean isInError()
+    {
+        IFieldTracking tracking = getComponentTracking();
+
+        return tracking != null && tracking.isInError();
+    }
+
+    public String getFieldInputValue()
+    {
+        IFieldTracking tracking = getComponentTracking();
+
+        return tracking == null ? null : tracking.getInput();
+    }
+
+    /**
+     * Returns all the field trackings as an unmodifiable List.
+     */
+
+    public List getFieldTracking()
+    {
+        if (Tapestry.size(_trackings) == 0)
+            return null;
+
+        return Collections.unmodifiableList(_trackings);
+    }
+
+    public void reset()
+    {
+        IFieldTracking tracking = getComponentTracking();
+
+        if (tracking != null)
+        {
+            _trackings.remove(tracking);
+
+            String formName = tracking.getComponent().getForm().getName();
+
+            Map formMap = (Map) _trackingMap.get(formName);
+
+            if (formMap != null)
+                formMap.remove(tracking.getFieldName());
+        }
+    }
+
+    /**
+     * Invokes {@link #record(String, ValidationConstraint)}, or
+     * {@link #record(IRender, ValidationConstraint)}if the
+     * {@link ValidatorException#getErrorRenderer() error renderer property}is not null.
+     */
+
+    public void record(ValidatorException ex)
+    {
+        IRender errorRenderer = ex.getErrorRenderer();
+
+        if (errorRenderer == null)
+            record(ex.getMessage(), ex.getConstraint());
+        else
+            record(errorRenderer, ex.getConstraint());
+    }
+
+    /**
+     * Invokes {@link #record(IRender, ValidationConstraint)}, after wrapping the message parameter
+     * in a {@link RenderString}.
+     */
+
+    public void record(String message, ValidationConstraint constraint)
+    {
+        record(new RenderString(message), constraint);
+    }
+
+    /**
+     * Records error information about the currently selected component, or records unassociated
+     * (with any field) errors.
+     * <p>
+     * Currently, you may have at most one error per <em>field</em> (note the difference between
+     * field and component), but any number of unassociated errors.
+     * <p>
+     * Subclasses may override the default error message (based on other factors, such as the field
+     * and constraint) before invoking this implementation.
+     * 
+     * @since 1.0.9
+     */
+
+    public void record(IRender errorRenderer, ValidationConstraint constraint)
+    {
+        FieldTracking tracking = findCurrentTracking();
+
+        // Note that recording two errors for the same field is not advised; the
+        // second will override the first.
+
+        tracking.setErrorRenderer(errorRenderer);
+        tracking.setConstraint(constraint);
+    }
+
+    public void recordFieldInputValue(String input)
+    {
+        FieldTracking tracking = findCurrentTracking();
+
+        tracking.setInput(input);
+    }
+
+    /**
+     * Finds or creates the field tracking for the {@link #setFormComponent(IFormComponent)}current
+     * component. If no current component, an unassociated error is created and returned.
+     * 
+     * @since 3.0
+     */
+
+    protected FieldTracking findCurrentTracking()
+    {
+        FieldTracking result = null;
+
+        if (_trackings == null)
+            _trackings = new ArrayList();
+
+        if (_trackingMap == null)
+            _trackingMap = new HashMap();
+
+        if (_currentComponent == null || _currentComponent.getName() == null)
+        {
+            result = new FieldTracking();
+
+            // Add it to the field trackings, but not to the
+            // map.
+
+            _trackings.add(result);
+        }
+        else
+        {
+            result = getComponentTracking();
+
+            if (result == null)
+            {
+                String formName = _currentComponent.getForm().getName();
+
+                Map formMap = (Map) _trackingMap.get(formName);
+
+                if (formMap == null)
+                {
+                    formMap = new HashMap();
+                    _trackingMap.put(formName, formMap);
+                }
+
+                String fieldName = _currentComponent.getName();
+
+                result = new FieldTracking(fieldName, _currentComponent);
+
+                _trackings.add(result);
+                formMap.put(fieldName, result);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Does nothing. Override in a subclass to decoreate fields.
+     */
+
+    public void writePrefix(IMarkupWriter writer, IRequestCycle cycle, IFormComponent component,
+            IValidator validator)
+    {
+    }
+
+    /**
+     * Does nothing. Override in a subclass to decorate fields.
+     */
+
+    public void writeAttributes(IMarkupWriter writer, IRequestCycle cycle,
+            IFormComponent component, IValidator validator)
+    {
+    }
+
+    /**
+     * Default implementation; if the current field is in error, then a suffix is written. The
+     * suffix is: <code>&amp;nbsp;&lt;font color="red"&gt;**&lt;/font&gt;</code>.
+     */
+
+    public void writeSuffix(IMarkupWriter writer, IRequestCycle cycle, IFormComponent component,
+            IValidator validator)
+    {
+        if (isInError())
+        {
+            writer.printRaw("&nbsp;");
+            writer.begin("font");
+            writer.attribute("color", "red");
+            writer.print("**");
+            writer.end();
+        }
+    }
+
+    public boolean getHasErrors()
+    {
+        return getFirstError() != null;
+    }
+
+    /**
+     * A convienience, as most pages just show the first error on the page.
+     * <p>
+     * As of release 1.0.9, this returns an instance of {@link IRender}, not a {@link String}.
+     */
+
+    public IRender getFirstError()
+    {
+        if (Tapestry.size(_trackings) == 0)
+            return null;
+
+        Iterator i = _trackings.iterator();
+
+        while (i.hasNext())
+        {
+            IFieldTracking tracking = (IFieldTracking) i.next();
+
+            if (tracking.isInError())
+                return tracking.getErrorRenderer();
+        }
+
+        return null;
+    }
+
+    /**
+     * Checks to see if the field is in error. This will <em>not</em> work properly in a loop, but
+     * is only used by {@link FieldLabel}. Therefore, using {@link FieldLabel}in a loop (where the
+     * {@link IFormComponent}is renderred more than once) will not provide correct results.
+     */
+
+    protected boolean isInError(IFormComponent component)
+    {
+        if (_trackingMap == null)
+            return false;
+
+        IForm form = component.getForm();
+        // if there is no form, the component cannot have been rewound or rendered into a form yet
+        // so assume it cannot have errors.
+        if (form == null)
+            return false;
+
+        String formName = form.getName();
+        Map formMap = (Map) _trackingMap.get(formName);
+
+        if (formMap == null)
+            return false;
+
+        IFieldTracking tracking = (IFieldTracking) formMap.get(component.getName());
+
+        return tracking != null && tracking.isInError();
+    }
+
+    /**
+     * Returns a {@link List}of {@link IFieldTracking}s. This is the master list of trackings,
+     * except that it omits and trackings that are not associated with a particular field. May
+     * return an empty list, or null.
+     * <p>
+     * Order is not determined, though it is likely the order in which components are laid out on in
+     * the template (this is subject to change).
+     */
+
+    public List getAssociatedTrackings()
+    {
+        int count = Tapestry.size(_trackings);
+
+        if (count == 0)
+            return null;
+
+        List result = new ArrayList(count);
+
+        for (int i = 0; i < count; i++)
+        {
+            IFieldTracking tracking = (IFieldTracking) _trackings.get(i);
+
+            if (tracking.getFieldName() == null)
+                continue;
+
+            result.add(tracking);
+        }
+
+        return result;
+    }
+
+    /**
+     * Like {@link #getAssociatedTrackings()}, but returns only the unassociated trackings.
+     * Unassociated trackings are new (in release 1.0.9), and are why interface
+     * {@link IFieldTracking}is not very well named.
+     * <p>
+     * The trackings are returned in an unspecified order, which (for the moment, anyway) is the
+     * order in which they were added (this could change in the future, or become more concrete).
+     */
+
+    public List getUnassociatedTrackings()
+    {
+        int count = Tapestry.size(_trackings);
+
+        if (count == 0)
+            return null;
+
+        List result = new ArrayList(count);
+
+        for (int i = 0; i < count; i++)
+        {
+            IFieldTracking tracking = (IFieldTracking) _trackings.get(i);
+
+            if (tracking.getFieldName() != null)
+                continue;
+
+            result.add(tracking);
+        }
+
+        return result;
+    }
+
+    /**
+     * Returns the {@link IFieldTracking}for the current component, if any. Useful
+     * when displaying error messages for individual fields.
+     * 
+     * @since 3.0.2
+     */
+    public IFieldTracking getCurrentFieldTracking()
+    {
+        return getComponentTracking();
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/ValidationStrings.properties b/tapestry-framework/src/org/apache/tapestry/valid/ValidationStrings.properties
new file mode 100644
index 0000000..83007bd
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/ValidationStrings.properties
@@ -0,0 +1,24 @@
+# $Id$
+
+field-is-required=You must enter a value for {0}.
+field-too-short=You must enter at least {0} characters for {1}.
+
+invalid-date-format=Invalid date format for {0}.  Format is {1}.
+invalid-int-format={0} must be an integer value.
+invalid-format={0} is not in a recognized format.
+invalid-numeric-format={0} must be a numeric value.
+
+date-too-early={0} must be on or after {1}.
+date-too-late={0} must be on or before {1}.
+
+number-too-small={0} must not be smaller than {1}.
+number-too-large={0} must not be larger than {1}.
+
+number-range={0} must be between {1} and {2}.
+
+invalid-email-format=Invalid email format for {0}.  Format is user@hostname.
+
+pattern-not-matched={0} does not fulfill the required pattern {1}.
+
+invalid-url-format = Invalid URL.
+disallowed-protocol = Disallowed protocol - protocol must be {0}.
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/ValidationStrings_de.properties b/tapestry-framework/src/org/apache/tapestry/valid/ValidationStrings_de.properties
new file mode 100644
index 0000000..00a4688
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/ValidationStrings_de.properties
@@ -0,0 +1,17 @@
+# $Id$
+
+field-is-required=Das Eingabefeld ''{0}'' ist ein Pflichtfeld.
+field-too-short=Sie müssen mindestens {0} Zeichen für das Eingabefeld ''{1}'' eingeben.
+
+invalid-date-format=Das Eingabefeld ''{0}'' hat ein falsches Datumsformat (Eingabeformat ist {1}).
+invalid-int-format=Das Eingabefeld ''{0}'' erwartet einen ganzzahligen Wert.
+invalid-format=Das Eingabefeld ''{0}'' hat nicht das gewünschte Format.
+invalid-numeric-format=Das Eingabefeld ''{0}'' erwartet einen numerischen Wert.
+
+date-too-early=Das Datum für das Eingabefeld ''{0}'' kann nur der {1} oder später sein.
+date-too-late=Das Datum für das Eingabefeld ''{0}'' kann nur der {1} oder früher sein.
+
+number-too-small=Der Wert für das Eingabefeld ''{0}'' darf nicht kleiner als {1} sein.
+number-too-large=Der Wert für das Eingabefeld ''{0}'' darf nicht größer als {1} sein.
+
+number-range=Der Wert für das Eingabefeld ''{0}'' darf nur zwischen {1} und {2} liegen.
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/ValidationStrings_es.properties b/tapestry-framework/src/org/apache/tapestry/valid/ValidationStrings_es.properties
new file mode 100644
index 0000000..a8e9c6d
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/ValidationStrings_es.properties
@@ -0,0 +1,19 @@
+# $Id$
+
+field-is-required=Tiene que ingresar un valor para {0}.
+field-too-short=Tiene que ingresar al menos {0} caracteres para {1}.
+
+invalid-date-format=Formato de fecha no válido para {0}.  El formato es {1}.
+invalid-int-format={0} tiene que ser un valor entero.
+invalid-format={0} no se encuentra en un formato reconocido.
+invalid-numeric-format={0} tiene que ser un valor numérico.
+
+date-too-early={0} tiene que ser actual o después de {1}.
+date-too-late={0} tiene que ser actual o antes de {1}.
+
+number-too-small={0} no puede ser menor que {1}.
+number-too-large={0} no puede ser mayor que {1}.
+
+number-range={0} tiene que estar entre {1} y {2}.
+
+invalid-email-format=Formato de email inválido para {0}.  El formato es usuario@servidor.
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/ValidationStrings_fi.properties b/tapestry-framework/src/org/apache/tapestry/valid/ValidationStrings_fi.properties
new file mode 100644
index 0000000..e40e8cc
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/ValidationStrings_fi.properties
@@ -0,0 +1,24 @@
+# $Id$
+
+field-is-required=Anna syöte kenttään: {0}.
+field-too-short=Kentän {1} arvon minimipituus on {0} merkkiä.
+
+invalid-date-format=Kentän {0} päivämäärä on väärää muotoa. Muoto on {1}.
+invalid-int-format=Kentän {0} arvon pitää olla kokonaisluku.
+invalid-format=Kentän {0} syöte on väärää muotoa.
+invalid-numeric-format=Kentän {0} arvon pitää olla luku.
+
+date-too-early=Kentän {0} päivämäärä ei saa olla ennen {1}.
+date-too-late=Kentän {0} päivämäärä ei saa olla jälkeen {1}.
+
+number-too-small=Kentän {0} arvo ei saa olla pienempi {1}.
+number-too-large=Kentän {0} arvo ei saa olla suurempi kuin {1}.
+
+number-range=Kentän {0} arvon tulee olla välillä {1}-{2}.
+
+invalid-email-format=Sähköpostiosoite kentässä {0} on väärää muotoa. Muoto on tunnus@kone.fi.
+
+pattern-not-matched=Kentän {0} arvo ei ole vaaditussa muodossa {1}.
+
+invalid-url-format = URL on väärää muotoa.
+disallowed-protocol = Protokolla ei kelpaa - protokollan pitää olla {0}.
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/ValidationStrings_sv.properties b/tapestry-framework/src/org/apache/tapestry/valid/ValidationStrings_sv.properties
new file mode 100644
index 0000000..83059f3
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/ValidationStrings_sv.properties
@@ -0,0 +1,24 @@
+# $Id$
+
+field-is-required=Du måste skriva in ett värde för {0}.
+field-too-short=Du måste skriva in minst {0} tecken för {1}.
+
+invalid-date-format=Ogilitigt datumformat för {0}. Formatet är {1}.
+invalid-int-format={0} måste vara ett heltalsvärde.
+invalid-format={0} är inte i ett känt format.
+invalid-numeric-format={0} måste vara ett numeriskt värde.
+
+date-too-early={0} måste vara på eller efter {1}.
+date-too-late={0} måste vara på eller före {1}.
+
+number-too-small={0} måste vara mindre än {1}.
+number-too-large={0} måste vara större än {1}.
+
+number-range={0} måste vara mellan {1} och {2}.
+
+invalid-email-format=Ogiltigt e-post address format för {0}. Korrekt format är användare@domän.
+
+pattern-not-matched={0} uppfyller inte det önskade mönstret. {1}.
+
+invalid-url-format=Ogiltig URL.
+disallowed-protocol=Protokollet är inte tillåtet - det måste vara {0}.
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/ValidationStrings_zh_CN.properties b/tapestry-framework/src/org/apache/tapestry/valid/ValidationStrings_zh_CN.properties
new file mode 100644
index 0000000..b2a8239
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/ValidationStrings_zh_CN.properties
@@ -0,0 +1,36 @@
+# Copyright 2005 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.
+
+field-is-required=\u8bf7\u8f93\u5165{0}\u7684\u5185\u5bb9\u3002
+field-too-short={1}\u7684\u5185\u5bb9\u4e0d\u80fd\u5c11\u4e8e{0}\u5b57\u7b26\u3002
+
+invalid-date-format={0}\u7684\u65e5\u671f\u683c\u5f0f\u4e0d\u6b63\u786e\uff0c\u6b63\u786e\u683c\u5f0f\u662f{1}\u3002
+invalid-int-format={0}\u7684\u5185\u5bb9\u5fc5\u987b\u662f\u6574\u6570\u3002
+invalid-format=\u65e0\u6cd5\u8bc6\u522b{0}\u7684\u683c\u5f0f\u3002
+invalid-numeric-format={0}\u7684\u5185\u5bb9\u5fc5\u987b\u662f\u6570\u5b57\u3002
+
+date-too-early={0}\u7684\u65e5\u671f\u5fc5\u987b\u5728{1}\u4e4b\u540e\u3002
+date-too-late={0}\u7684\u65e5\u671f\u5fc5\u987b\u5728{1}\u4e4b\u524d\u3002
+
+number-too-small={0}\u7684\u6570\u503c\u4e0d\u80fd\u5c0f\u4e8e{1}\u3002
+number-too-large={0}\u7684\u6570\u503c\u4e0d\u80fd\u5927\u4e8e{1}\u3002
+
+number-range={0}\u7684\u6570\u503c\u5fc5\u987b\u5728{1}\u548c{2}\u4e4b\u95f4\u3002
+
+invalid-email-format=\u7535\u5b50\u90ae\u4ef6\u5730\u5740{0}\u683c\u5f0f\u4e0d\u6b63\u786e\uff0c\u6b63\u786e\u7684\u683c\u5f0f\u662fuser@hostname\u3002
+
+pattern-not-matched={0}\u4e0d\u7b26\u5408\u6240\u8981\u6c42\u7684\u683c\u5f0f{1}\u3002
+
+invalid-url-format = \u9519\u8bef\u7684URL\u3002
+disallowed-protocol = \u4e0d\u88ab\u5141\u8bb8\u7684\u534f\u8bae\uff0c\u5fc5\u987b\u662f{0}\u3002
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/ValidationStrings_zh_TW.properties b/tapestry-framework/src/org/apache/tapestry/valid/ValidationStrings_zh_TW.properties
new file mode 100644
index 0000000..fde9e7a
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/ValidationStrings_zh_TW.properties
@@ -0,0 +1,36 @@
+# Copyright 2005 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.
+
+field-is-required=\u8acb\u8f38\u5165{0}\u6b04\u4f4d\u7684\u5167\u5bb9\u3002
+field-too-short={1}\u6b04\u4f4d\u7684\u5167\u5bb9\u4e0d\u80fd\u5c11\u65bc{0}\u5b57\u5143\u3002
+
+invalid-date-format={0}\u6b04\u4f4d\u7684\u65e5\u671f\u683c\u5f0f\u4e0d\u6b63\u78ba\uff0c\u6b63\u78ba\u683c\u5f0f\u70ba{1}\u3002
+invalid-int-format={0}\u6b04\u4f4d\u7684\u5167\u5bb9\u5fc5\u9808\u662f\u6574\u6578\u3002
+invalid-format=\u7121\u6cd5\u8fa8\u8a8d{0}\u6b04\u4f4d\u7684\u683c\u5f0f\u3002
+invalid-numeric-format={0}\u6b04\u4f4d\u7684\u5167\u5bb9\u5fc5\u9808\u662f\u6578\u5b57\u3002
+
+date-too-early={0}\u6b04\u4f4d\u7684\u65e5\u671f\u5fc5\u9808\u5728{1}\u4e4b\u5f8c\u3002
+date-too-late={0}\u6b04\u4f4d\u7684\u65e5\u671f\u5fc5\u9808\u5728{1}\u4e4b\u524d\u3002
+
+number-too-small={0}\u6b04\u4f4d\u7684\u6578\u5b57\u4e0d\u80fd\u5c0f\u65bc{1}\u3002
+number-too-large={0}\u6b04\u4f4d\u7684\u6578\u5b57\u4e0d\u80fd\u5927\u65bc{1}\u3002
+
+number-range={0}\u6b04\u4f4d\u7684\u6578\u5b57\u5fc5\u9808\u5728{1}\u548c{2}\u4e4b\u9593\u3002
+
+invalid-email-format=\u96fb\u5b50\u90f5\u4ef6\u5730\u5740{0}\u683c\u5f0f\u932f\u8aa4\uff0c\u6b63\u78ba\u7684\u683c\u5f0f\u662fuser@hostname\u3002
+
+pattern-not-matched={0}\u4e0d\u7b26\u5408\u6240\u8981\u6c42\u7684\u6a23\u5f0f{1}\u3002
+
+invalid-url-format = \u932f\u8aa4\u7684URL\u3002
+disallowed-protocol = \u4e0d\u88ab\u5141\u8a31\u7684\u5354\u5b9a\uff0c\u5fc5\u9808\u662f{0}\u3002
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/Validator.js b/tapestry-framework/src/org/apache/tapestry/valid/Validator.js
new file mode 100644
index 0000000..6508436
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/Validator.js
@@ -0,0 +1,18 @@
+// $Id: Validator.js,v 1.1 2002/09/07 13:03:24 hship Exp $

+//

+// Simple functions to support input field validation in Tapestry.

+

+function validator_invalid_field(field, message)

+{

+  field.focus();

+  field.select();

+  

+  window.alert(message);

+  

+  return false;

+}

+

+function regexpTestUrl(sUrl) {

+	var regexp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/

+	return regexp.test(sUrl);

+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/ValidatorException.java b/tapestry-framework/src/org/apache/tapestry/valid/ValidatorException.java
new file mode 100644
index 0000000..c9f0643
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/ValidatorException.java
@@ -0,0 +1,75 @@
+//  Copyright 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.tapestry.valid;
+
+import org.apache.tapestry.IRender;
+
+/**
+ *  Thrown by a {@link IValidator} when submitted input is not valid.
+ *
+ *  @author Howard Lewis Ship
+ *  @version $Id$
+ *  @since 1.0.8
+ *
+ **/
+
+public class ValidatorException extends Exception
+{
+    private IRender _errorRenderer;
+    private ValidationConstraint _constraint;
+
+    public ValidatorException(String errorMessage)
+    {
+        this(errorMessage, null, null);
+    }
+
+    public ValidatorException(String errorMessage, ValidationConstraint constraint)
+    {
+        this(errorMessage, null, constraint);
+    }
+
+    /**
+     *  Creates a new instance.
+     *  @param errorMessage the default error message to be used (this may be
+     *  overriden by the {@link IValidationDelegate})
+     *  @param errorRenderer to use to render the error message (may be null)
+     *  @param constraint a validation constraint that has been compromised, or
+     *  null if no constraint is applicable
+     * 
+     **/
+
+    public ValidatorException(
+        String errorMessage,
+        IRender errorRenderer,
+        ValidationConstraint constraint)
+    {
+        super(errorMessage);
+
+        _errorRenderer = errorRenderer;
+        _constraint = constraint;
+    }
+
+    public ValidationConstraint getConstraint()
+    {
+        return _constraint;
+    }
+
+    /** @since 3.0 **/
+
+    public IRender getErrorRenderer()
+    {
+        return _errorRenderer;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/valid/package.html b/tapestry-framework/src/org/apache/tapestry/valid/package.html
new file mode 100644
index 0000000..f858edf
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/valid/package.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+
+<html>
+<body>
+
+Components and classes that provide specialized, validating text fields.
+The component {@link org.apache.tapestry.valid.ValidField}
+does most of the work, and is paired with an implementation of
+{@link org.apache.tapestry.valid.IValidator} (often as a helper bean)
+which provides the rules of translation (between object value and string) and validation.
+
+<p>
+Fields can all be set as required or not; most IValidator implementations add additional
+validations, such as fitting the input value between a minimum and maximum value.
+
+
+<p>Fields can also have a {@link org.apache.tapestry.valid.FieldLabel} that reflects the state (normal or error)
+of the field.
+
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+
+</body>
+</html>
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/AbstractPostfield.java b/tapestry-framework/src/org/apache/tapestry/wml/AbstractPostfield.java
new file mode 100644
index 0000000..f1d661a
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/AbstractPostfield.java
@@ -0,0 +1,121 @@
+//  Copyright 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.tapestry.wml;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IForm;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.form.AbstractFormComponent;
+
+/**
+ *  A base class for building components that correspond to WML postfield elements.
+ *  All such components must be wrapped (directly or indirectly) by
+ *  a {@link Go} component.
+ *
+ *  @version $Id$
+ *  @author David Solis
+ *  @since 3.0
+ *
+ **/
+
+public abstract class AbstractPostfield extends AbstractFormComponent
+{
+
+    /**
+     *  Returns the {@link org.apache.tapestry.wml.Go} wrapping this component.
+     *
+     *  @throws  ApplicationRuntimeException if the component is not wrapped by a
+     * {@link org.apache.tapestry.wml.Go}.
+     *
+     **/
+
+    public IForm getForm(IRequestCycle cycle)
+    {
+        IForm result = Go.get(cycle);
+
+        if (result == null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("Postfield.must-be-contained-by-go"),
+                this,
+                null,
+                null);
+
+        setForm(result);
+
+        return result;
+    }
+
+    /**
+     *  @see org.apache.tapestry.AbstractComponent#renderComponent(IMarkupWriter, IRequestCycle)
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        IForm form = getForm(cycle);
+
+        boolean rewinding = form.isRewinding();
+
+        if (!rewinding && cycle.isRewinding())
+            return;
+
+        String name = form.getElementId(this);
+
+        if (rewinding)
+        {
+            rewind(cycle);
+            return;
+        }
+
+        writer.beginEmpty("postfield");
+
+        writer.attribute("name", name);
+        String varName = getVarName();
+        writer.attributeRaw("value", varName != null ? getEncodedVarName(varName) : "");
+
+        renderInformalParameters(writer, cycle);
+
+        writer.closeTag();
+    }
+
+    protected abstract void rewind(IRequestCycle cycle);
+
+    private String getEncodedVarName(String varName)
+    {
+        return "$(" + varName + ")";
+    }
+
+    public boolean isDisabled()
+    {
+        return false;
+    }
+
+    public abstract String getVarName();
+
+    public abstract IBinding getValueBinding();
+
+    public void updateValue(Object value)
+    {
+        getValueBinding().setObject(value);
+    }
+
+    public abstract IForm getForm();
+    public abstract void setForm(IForm form);
+
+    public abstract String getName();
+    public abstract void setName(String name);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/Card.java b/tapestry-framework/src/org/apache/tapestry/wml/Card.java
new file mode 100644
index 0000000..9e733fa
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/Card.java
@@ -0,0 +1,72 @@
+//  Copyright 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.tapestry.wml;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  A deck contains a collection of cards. There is a variety of card types, each specifying a different mode of
+ *  user interaction.
+ *
+ *  @version $Id$
+ *  @author David Solis
+ *  @since 3.0
+ *
+ **/
+
+public abstract class Card extends AbstractComponent
+{
+    private static final String ATTRIBUTE_NAME = "org.apache.tapestry.wml.Card";
+
+    /**
+     *  @see AbstractComponent#renderComponent(IMarkupWriter, IRequestCycle)
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        if (cycle.getAttribute(ATTRIBUTE_NAME) != null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("Card.cards-may-not-nest"),
+                this,
+                null,
+                null);
+
+        cycle.setAttribute(ATTRIBUTE_NAME, this);
+
+        writer.begin("card");
+
+        String title = getTitle();
+        if (Tapestry.isNonBlank(title))
+            writer.attribute("title", title);
+
+        renderInformalParameters(writer, cycle);
+
+        IMarkupWriter nestedWriter = writer.getNestedWriter();
+
+        renderBody(nestedWriter, cycle);
+
+        nestedWriter.close();
+
+        writer.end();
+
+        cycle.removeAttribute(ATTRIBUTE_NAME);
+    }
+
+    public abstract String getTitle();
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/Card.jwc b/tapestry-framework/src/org/apache/tapestry/wml/Card.jwc
new file mode 100644
index 0000000..4d1101a
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/Card.jwc
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.wml.Card">
+
+    <description>
+        An WML deck contains a collection of cards. There is a variety of card types, each specifying a different mode
+        of user interaction.
+    </description>
+
+    <parameter name="title" type="java.lang.String" direction="in">
+        <description>
+            The title attribute specifies advisory information about the element. The title may be rendered in a
+            variety of ways by the user agent (eg, suggested bookmark name, pop-up tooltip, etc.).
+        </description>
+    </parameter>
+
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/Deck.java b/tapestry-framework/src/org/apache/tapestry/wml/Deck.java
new file mode 100644
index 0000000..369dd6b
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/Deck.java
@@ -0,0 +1,45 @@
+//  Copyright 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.tapestry.wml;
+
+import java.io.OutputStream;
+
+import org.apache.tapestry.AbstractPage;
+import org.apache.tapestry.IMarkupWriter;
+
+/**
+ *  Concrete class for WML decks. Most decks
+ *  should be able to simply subclass this, adding new properties and
+ *  methods.  An unlikely exception would be a deck that was not based
+ *  on a template.
+ *
+ *  @version $Id$
+ *  @author David Solis
+ *  @since 0.2.9
+ * 
+ **/
+
+public class Deck extends AbstractPage
+{
+    /**
+     *  Returns a new {@link WMLWriter}.
+     *
+     **/
+    public IMarkupWriter getResponseWriter(OutputStream out)
+    {
+        return new WMLWriter(out, getOutputEncoding());
+    }
+
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/Do.java b/tapestry-framework/src/org/apache/tapestry/wml/Do.java
new file mode 100644
index 0000000..7bfbed1
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/Do.java
@@ -0,0 +1,68 @@
+//  Copyright 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.tapestry.wml;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  The do element provides a general mechanism for the user to act upon the current card,
+ *  in other words a card-level user interface element.
+ *  The representation of the do element is user agent dependent and the author must only assume
+ *  that the element is mapped to a unique user interface widget that the user can activate.
+ *  For example, the widget mapping may be to a graphically rendered button, a soft or function key, a voice-activated command sequence, or any other interface that has a simple "activate" operation with no inter-operation persistent state.
+ *  The do element may appear at both the card and deck-level.
+ *
+ *  @version $Id$
+ *  @author David Solis
+ *  @since 3.0
+ *
+ **/
+
+public abstract class Do extends AbstractComponent
+{
+    /**
+     *  @see AbstractComponent#renderComponent(IMarkupWriter, IRequestCycle)
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        boolean render = !cycle.isRewinding();
+
+        if (render)
+        {
+            writer.begin("do");
+
+            writer.attribute("type", getType());
+
+            String label = getLabel();
+            if (Tapestry.isNonBlank(label))
+                writer.attribute("label", label);
+
+            renderInformalParameters(writer, cycle);
+        }
+
+        renderBody(writer, cycle);
+
+        if (render)
+            writer.end();
+    }
+
+    public abstract String getType();
+
+    public abstract String getLabel();
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/Do.jwc b/tapestry-framework/src/org/apache/tapestry/wml/Do.jwc
new file mode 100644
index 0000000..f792b03
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/Do.jwc
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.wml.Do">
+
+    <description>
+        The do element provides a general mechanism for the user to act upon the current card, i.e. a card-level user
+        interface element.
+    </description>
+
+    <parameter name="type" type="java.lang.String" direction="in" required="yes" >
+        <description>
+            The do element type. This attribute provides a hint to the user agent about the author's intended use of
+            the element and how the element should be mapped to a physical user interface construct.
+            Predefined DO types are accept, prev, help, reset, options, delete and unkown.
+        </description>
+    </parameter>
+
+    <parameter name="label" type="java.lang.String" direction="in">
+        <description>
+            If the user agent is able to dynamically label the user interface widget, this attribute specifies a textual
+            string suitable for such labelling.
+        </description>
+    </parameter>
+
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/Go.java b/tapestry-framework/src/org/apache/tapestry/wml/Go.java
new file mode 100644
index 0000000..fa1c0d8
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/Go.java
@@ -0,0 +1,92 @@
+//  Copyright 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.tapestry.wml;
+
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.engine.ILink;
+import org.apache.tapestry.form.Form;
+import org.apache.tapestry.valid.IValidationDelegate;
+
+/**
+ *  The go element declares a go task, indicating navigation to a URI. If the URI
+ *  names a WML card or deck, it is displayed. 
+ *
+ *  @version $Id$
+ *  @author David Solis
+ *  @since 3.0
+ *
+ **/
+
+public abstract class Go extends Form
+{
+
+    /** @since 3.0 **/
+
+    protected void writeAttributes(IMarkupWriter writer, ILink link)
+    {
+        String method = getMethod();
+
+        writer.begin(getTag());
+        writer.attribute("method", (method == null) ? "post" : method);
+        writer.attribute("href", link.getURL(null, false));
+    }
+
+    /** @since 3.0 **/
+
+    protected void writeHiddenField(IMarkupWriter writer, String name, String value)
+    {
+        writer.beginEmpty("postfield");
+        writer.attribute("name", name);
+        writer.attribute("value", value);
+        writer.println();
+    }
+
+    /**
+     *  This component doesn't support event handlers.
+     *
+     **/
+    protected void emitEventHandlers(IMarkupWriter writer, IRequestCycle cycle)
+    {
+    }
+
+    /**
+     *  This component doesn't support delegate.
+     *
+     **/
+    public IValidationDelegate getDelegate()
+    {
+        return null;
+    }
+
+    public void setDelegate(IValidationDelegate delegate)
+    {
+        throw new ApplicationRuntimeException(
+            Tapestry.format("unsupported-property", this, "delegate"));
+    }
+
+    protected String getTag()
+    {
+        return "go";
+    }
+
+
+    protected String getDisplayName()
+    {
+        return "Go";
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/Go.jwc b/tapestry-framework/src/org/apache/tapestry/wml/Go.jwc
new file mode 100644
index 0000000..d06bf96
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/Go.jwc
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN"
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.wml.Go">
+
+    <description>
+        The go element declares a go task, indicating navigation to a URI.
+    </description>
+
+    <parameter name="method" type="java.lang.String" direction="in">
+        <description>
+            The method used by the form when it is submitted, defaults to POST.
+        </description>
+    </parameter>
+
+    <parameter name="listener" type="org.apache.tapestry.IActionListener" required="no" direction="in">
+        <description>
+            Object invoked when the form is submitted, after all form components have responded
+            to the submission.
+        </description>
+    </parameter>
+
+    <parameter name="stateful" type="boolean" direction="custom">
+        <description>
+            If true (the default), then an active HttpSession is required.
+        </description>
+    </parameter>
+
+    <parameter name="direct" type="boolean" direction="in">
+        <description>
+            If true (the default), then the more efficient direct service is used.
+            If false, then the action service is used.
+        </description>
+    </parameter>
+
+    <reserved-parameter name="href"/>
+
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/GoLinkRenderer.java b/tapestry-framework/src/org/apache/tapestry/wml/GoLinkRenderer.java
new file mode 100644
index 0000000..ca3f83b
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/GoLinkRenderer.java
@@ -0,0 +1,46 @@
+//  Copyright 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.tapestry.wml;
+
+import org.apache.tapestry.link.DefaultLinkRenderer;
+import org.apache.tapestry.link.ILinkRenderer;
+
+/**
+ *  A subclass of {@link org.apache.tapestry.link.DefaultLinkRenderer} for
+ *  the WML Go element.
+ *
+ *  @author David Solis
+ *  @version $Id$
+ *  @since 3.0
+ **/
+public class GoLinkRenderer extends DefaultLinkRenderer
+{
+
+	/**
+	 *  A singleton for the go link. 
+	 **/
+
+	 public static final ILinkRenderer SHARED_INSTANCE = new GoLinkRenderer();
+
+    public String getElement()
+    {
+        return "go";
+    }
+
+    public boolean getHasBody()
+    {
+        return false;
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/Image.java b/tapestry-framework/src/org/apache/tapestry/wml/Image.java
new file mode 100644
index 0000000..86e6dd9
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/Image.java
@@ -0,0 +1,59 @@
+//  Copyright 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.tapestry.wml;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.IAsset;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  The Image component indicates that an image is to be included in the text flow. Image layout is done within the
+ *  context of normal text layout.
+ *
+ *  @version $Id$
+ *  @author David Solis
+ *  @since 3.0
+ *
+ **/
+
+public abstract class Image extends AbstractComponent
+{
+    /**
+     *  @see org.apache.tapestry.AbstractComponent#renderComponent(org.apache.tapestry.IMarkupWriter, org.apache.tapestry.IRequestCycle)
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        boolean render = !cycle.isRewinding();
+
+        if (render)
+        {
+            writer.beginEmpty("img");
+
+            writer.attribute("src", getImage().buildURL(cycle));
+
+            writer.attribute("alt", getAlt());
+
+            renderInformalParameters(writer, cycle);
+
+            writer.closeTag();
+        }
+    }
+
+    public abstract IAsset getImage();
+
+	public abstract String getAlt();
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/Image.jwc b/tapestry-framework/src/org/apache/tapestry/wml/Image.jwc
new file mode 100644
index 0000000..797e67c
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/Image.jwc
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.wml.Image" allow-body="no">
+
+    <description>
+        Displays a wml image, deriving the source URL for the image from an asset.
+    </description>
+
+    <parameter name="image" type="org.apache.tapestry.IAsset" direction="in" required="yes">
+        <description>
+            The asset to display.
+        </description>
+    </parameter>
+
+    <parameter name="alt" type="java.lang.String" direction="in" required="yes">
+        <description>
+            This attribute specifies an alternative textual representation for the image. This representation is used
+            when the image can not be displayed using any other method
+        </description>
+    </parameter>
+
+    <reserved-parameter name="src"/>
+
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/Input.java b/tapestry-framework/src/org/apache/tapestry/wml/Input.java
new file mode 100644
index 0000000..92a83c5
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/Input.java
@@ -0,0 +1,90 @@
+//  Copyright 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.tapestry.wml;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  The Input element specifies a text entry object.
+ *
+ *  @version $Id$
+ *  @author David Solis
+ *  @since 3.0
+ *
+ **/
+
+public abstract class Input extends AbstractComponent
+{
+
+	/**
+	 *  @see AbstractComponent#renderComponent(IMarkupWriter, IRequestCycle)
+	 **/
+
+	protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+	{
+		boolean render = !cycle.isRewinding();
+
+		if (render)
+		{
+			writer.beginEmpty("input");
+
+			writer.attribute("type", isHidden() ? "password" : "text");
+
+            writer.attribute("name", getName());
+
+			String title = getTitle();
+			if (Tapestry.isNonBlank(title))
+				writer.attribute("title", title);
+
+			String format = getFormat();
+            if (Tapestry.isNonBlank(format))
+				writer.attribute("format", format);
+
+            boolean emptyok = isEmptyok();
+            if (emptyok != false)
+				writer.attribute("emptyok", emptyok);
+
+			renderInformalParameters(writer, cycle);
+
+			String value = readValue();
+			if (Tapestry.isNonBlank(value))
+				writer.attribute("value", value);
+
+			writer.closeTag();
+		}
+	}
+
+	public abstract String getTitle();
+
+	public abstract String getName();
+
+	public abstract String getFormat();
+
+	public abstract boolean isHidden();
+
+	public abstract boolean isEmptyok();
+
+	public abstract IBinding getValueBinding();
+
+	public String readValue()
+    {
+		return getValueBinding().getString();
+	}
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/Input.jwc b/tapestry-framework/src/org/apache/tapestry/wml/Input.jwc
new file mode 100644
index 0000000..5fc4c88
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/Input.jwc
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.wml.Input" allow-body="no">
+
+    <description>
+        The Input element specifies a text entry object.
+    </description>
+
+    <parameter name="title" type="java.lang.String" direction="in">
+        <description>
+            The title attribute specifies advisory information about the element. The title may be rendered in a
+            variety of ways by the user agent (eg, suggested bookmark name, pop-up tooltip, etc.).
+        </description>
+    </parameter>
+
+    <parameter name="hidden" type="boolean" direction="in"/>
+
+    <parameter name="name" type="java.lang.String" direction="in" required="yes" >
+        <description>
+            The name attribute specifies a variable name.
+        </description>
+    </parameter>
+
+    <parameter name="format" type="java.lang.String" direction="in">
+        <description>
+            The format attribute specifies an input mask for user input entries. The string consists of mask control
+            characters and static text that is displayed in the input area. The user agent may use the format mask to
+            facilitate accelerated data input. An input mask is only valid when it contains only legal format codes.
+            User agents must ignore invalid masks.
+            The format control characters specify the data format expected to be entered by the user.
+            See the WML specification for valid format control characters.
+        </description>
+    </parameter>
+
+    <parameter name="emptyok" type="boolean" direction="in">
+        <description>
+            The emptyok attribute indicates that this input element accepts empty input although a non-empty format
+            string has been specified.
+        </description>
+    </parameter>
+
+    <parameter name="value" type="java.lang.String" required="yes">
+        <description>
+            Bind value to the variable that should recieve the user input string.
+        </description>
+    </parameter>
+
+    <reserved-parameter name="type"/>
+
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/NestedWMLWriter.java b/tapestry-framework/src/org/apache/tapestry/wml/NestedWMLWriter.java
new file mode 100644
index 0000000..414526e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/NestedWMLWriter.java
@@ -0,0 +1,68 @@
+//  Copyright 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.tapestry.wml;
+
+import java.io.CharArrayWriter;
+import java.io.PrintWriter;
+
+import org.apache.tapestry.IMarkupWriter;
+
+/**
+ *  Subclass of {@link org.apache.tapestry.wml.WMLWriter} that is nested.  A nested writer
+ *  buffers its output, then inserts it into its parent writer when it is
+ *  closed.
+ *
+ *  @version $Id$
+ *  @author David Solis
+ *  @since 0.2.9
+ * 
+ **/
+
+public class NestedWMLWriter extends WMLWriter
+{
+    private IMarkupWriter _parent;
+    private CharArrayWriter _internalBuffer;
+
+    public NestedWMLWriter(IMarkupWriter parent)
+    {
+        super(parent.getContentType());
+
+        _parent = parent;
+
+        _internalBuffer = new CharArrayWriter();
+
+       setWriter(new PrintWriter(_internalBuffer));
+    }
+
+    /**
+     *  Invokes the {@link WMLWriter#close() super-class
+     *  implementation}, then gets the data accumulated in the
+     *  internal buffer and provides it to the containing writer using
+     *  {@link IMarkupWriter#printRaw(char[], int, int)}.
+     *
+     **/
+
+    public void close()
+    {
+        super.close();
+
+        char[] data = _internalBuffer.toCharArray();
+
+        _parent.printRaw(data, 0, data.length);
+
+        _internalBuffer = null;
+        _parent = null;
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/OnEvent.java b/tapestry-framework/src/org/apache/tapestry/wml/OnEvent.java
new file mode 100644
index 0000000..2fc91fa
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/OnEvent.java
@@ -0,0 +1,62 @@
+//  Copyright 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.tapestry.wml;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  The onevent element binds a task to a particular intrinsic event for the immediately enclosing element, ie,
+ *  specifying an onevent element inside an "XYZ" element associates an intrinsic event binding with the "XYZ" element.
+ *  The user agent must ignore any onevent element specifying a type that does not correspond to a legal intrinsic event
+ *  for the immediately enclosing element.
+ *
+ *  @version $Id$
+ *  @author David Solis
+ *  @since 3.0
+ *
+ **/
+
+public abstract class OnEvent extends AbstractComponent
+{
+    /**
+     *  @see AbstractComponent#renderComponent(IMarkupWriter, IRequestCycle)
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        boolean render = !cycle.isRewinding();
+
+        if (render)
+        {
+            writer.begin("onevent");
+
+            writer.attribute("type", getType());
+
+            renderInformalParameters(writer, cycle);
+        }
+
+        renderBody(writer, cycle);
+
+        if (render)
+        {
+            writer.end();
+        }
+    }
+
+    public abstract String getType();
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/OnEvent.jwc b/tapestry-framework/src/org/apache/tapestry/wml/OnEvent.jwc
new file mode 100644
index 0000000..576e48b
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/OnEvent.jwc
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.wml.OnEvent">
+
+    <description>
+        The onevent element binds a task to a particular intrinsic event for the immediately enclosing element, ie,
+        specifying an onevent element inside an "XYZ" element associates an intrinsic event binding with the "XYZ"
+        element.
+        The user agent must ignore any onevent element specifying a type that does not correspond to a legal
+        intrinsic event for the immediately enclosing element.
+    </description>
+
+    <parameter name="type" type="java.lang.String" direction="in" required="yes">
+        <description>
+            The type attribute indicates the name of the intrinsic event
+            (ontimer, onenterforward, onenterbackward, onpick).
+        </description>
+    </parameter>
+
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/Option.java b/tapestry-framework/src/org/apache/tapestry/wml/Option.java
new file mode 100644
index 0000000..e18d7f7
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/Option.java
@@ -0,0 +1,64 @@
+//  Copyright 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.tapestry.wml;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  This component serves as a container for one item that is listed as a choice in a {@link Select}. A {@link Select}
+ *  offers a selection of choices from which usersu may choose one or more items. The select list is created using a
+ *  select element which contains a collection of option elements. A string or text describing the item appears between
+ *  the opening and closing option tags.
+ *
+ *  In order to have a dynamic onpick attribute it is better to use a concrete class of
+ *  {@link org.apache.tapestry.link.ILinkRenderer} with the {@link OptionRenderer}.
+ *
+ *  @version $Id$
+ *  @author David Solis
+ *  @since 3.0
+ *
+ **/
+
+public abstract class Option extends AbstractComponent {
+
+	/**
+	 *  @see org.apache.tapestry.AbstractComponent#renderComponent(IMarkupWriter, IRequestCycle)
+	 **/
+
+	protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        boolean render = !cycle.isRewinding();
+
+        if (render)
+        {
+            writer.begin("option");
+
+            String value = getValue();
+            if (Tapestry.isNonBlank(value))
+                writer.attribute("value", value);
+
+            renderInformalParameters(writer, cycle);
+
+            renderBody(writer, cycle);
+
+            writer.end();
+        }
+	}
+
+    public abstract String getValue();
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/Option.jwc b/tapestry-framework/src/org/apache/tapestry/wml/Option.jwc
new file mode 100644
index 0000000..fdf5cc4
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/Option.jwc
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN"
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.wml.Option">
+
+    <description>
+        This component serves as a container for one item that is listed as a choice in a Select. A Select
+        offers a selection of choices from which usersu may choose one or more items. The select list is created using a
+        select element which contains a collection of option elements. A string or text describing the item appears between
+        the opening and closing option tags.
+    </description>
+
+    <parameter name="value" type="java.lang.String" direction="in">
+        <description>
+            The value attribute specifies the value to be used when setting the name variable. When the user selects
+            this option, the resulting value specified in the value attribute is used to set the Select element's
+            name wml variable.
+        </description>
+    </parameter>
+
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/OptionRenderer.java b/tapestry-framework/src/org/apache/tapestry/wml/OptionRenderer.java
new file mode 100644
index 0000000..6f41dfc
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/OptionRenderer.java
@@ -0,0 +1,49 @@
+//  Copyright 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.tapestry.wml;
+
+import org.apache.tapestry.link.DefaultLinkRenderer;
+import org.apache.tapestry.link.ILinkRenderer;
+
+/**
+ *  Implementation of {@link org.apache.tapestry.link.ILinkRenderer} for
+ *  the WML Option element.
+ *
+ *  The value attribute is reserved.
+ *
+ *  @version $Id$
+ *  @author David Solis
+ *  @since 3.0
+ *
+ **/
+
+public class OptionRenderer extends DefaultLinkRenderer
+{
+	/**
+	 *  A singleton for the option link.
+	 **/
+
+	 public static final ILinkRenderer SHARED_INSTANCE = new OptionRenderer();
+
+    protected String getElement()
+    {
+        return "option";
+    }
+
+    protected String getUrlAttribute()
+    {
+        return "onpick";
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/Postfield.java b/tapestry-framework/src/org/apache/tapestry/wml/Postfield.java
new file mode 100644
index 0000000..c24db09
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/Postfield.java
@@ -0,0 +1,37 @@
+//  Copyright 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.tapestry.wml;
+
+import org.apache.tapestry.IRequestCycle;
+
+/**
+ *  The postfield element specifies a field name and value for transmission to an origin server during a URL request.
+ *  @see Go
+ *
+ *  @version $Id$
+ *  @author David Solis
+ *  @since 3.0
+ *
+ **/
+
+public abstract class Postfield extends AbstractPostfield
+{
+
+    protected void rewind(IRequestCycle cycle)
+    {
+        String value = cycle.getRequestContext().getParameter(getName());
+        updateValue(value);
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/Postfield.jwc b/tapestry-framework/src/org/apache/tapestry/wml/Postfield.jwc
new file mode 100644
index 0000000..318e1b8
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/Postfield.jwc
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.wml.Postfield" allow-body="no">
+
+    <description>
+        The postfield element specifies a field name and value for transmission to an origin server during an
+        URL request.
+    </description>
+
+    <parameter name="value" type="java.lang.String" required="yes">
+        <description>
+            Bind value to the variable that should recieve the user input string.
+        </description>
+    </parameter>
+
+    <parameter name="name" property-name="varName" type="java.lang.String" direction="in" required="yes">
+        <description>
+            The name attribute specifies an WML variable name.
+        </description>
+    </parameter>
+
+    <reserved-parameter name="value"/>
+
+    <property-specification name="name" type="java.lang.String"/>
+    <property-specification name="form" type="org.apache.tapestry.IForm"/>
+
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/PropertySelection.java b/tapestry-framework/src/org/apache/tapestry/wml/PropertySelection.java
new file mode 100644
index 0000000..c20cd84
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/PropertySelection.java
@@ -0,0 +1,76 @@
+//  Copyright 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.tapestry.wml;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.form.IPropertySelectionModel;
+
+/**
+ *  A high level component used to render a drop-down list of options that the user may select.
+ *
+ *  Informal parameters are applied to the <select> tag.  To have greater control over the <option> tags, you must use
+ *  a Select and Option or a concrete class of {@link org.apache.tapestry.link.ILinkRenderer} with the
+ *  {@link OptionRenderer}.
+ *
+ *  @version $Id$
+ *  @author David Solis
+ */
+
+public abstract class PropertySelection extends AbstractComponent
+{
+    /**
+     *  @see AbstractComponent#renderComponent(IMarkupWriter, IRequestCycle)
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        boolean render = !cycle.isRewinding();
+
+        if (render)
+        {
+            IPropertySelectionModel model = getModel();
+
+            writer.begin("select");
+
+            writer.attribute("name", getName());
+
+            renderInformalParameters(writer, cycle);
+
+            writer.println();
+
+            int count = model.getOptionCount();
+
+            for (int i = 0; i < count; i++)
+            {
+
+                writer.begin("option");
+                writer.attribute("value", model.getValue(i));
+
+                writer.print(model.getLabel(i));
+
+                writer.end();
+                writer.println();
+            }
+
+            writer.end();
+        }
+    }
+
+    public abstract IPropertySelectionModel getModel();
+
+    public abstract String getName();
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/PropertySelection.jwc b/tapestry-framework/src/org/apache/tapestry/wml/PropertySelection.jwc
new file mode 100644
index 0000000..8a38fbc
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/PropertySelection.jwc
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN"
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.wml.PropertySelection"
+    allow-body="no"
+    allow-informal-parameters="yes">
+
+    <description>
+        Creates an WML select to choose a single property from a list of options.
+    </description>
+
+    <parameter name="name" required="yes" type="java.lang.String" direction="in"/>
+
+    <parameter name="model"
+        type="org.apache.tapestry.form.IPropertySelectionModel"
+        required="yes"
+        direction="auto"/>
+
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/Select.java b/tapestry-framework/src/org/apache/tapestry/wml/Select.java
new file mode 100644
index 0000000..0c092c5
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/Select.java
@@ -0,0 +1,102 @@
+//  Copyright 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.tapestry.wml;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  The Select element lets users pick from a list of options. Each option
+ *  is specified by an Option element. Each Option element may have one
+ *  line of formatted text (which may be wrapped or truncated by the user
+ *  agent if too long).
+ *
+ *  Unless multiple selections are required it is generally easier to use the {@link PropertySelection} component.
+ *
+ *  @version $Id$
+ *  @author David Solis
+ *  @since 3.0
+ *
+ **/
+public abstract class Select extends AbstractComponent
+{
+    /**
+     *  Used by the <code>Select</code> to record itself as a
+     *  {@link IRequestCycle} attribute, so that the
+     *  {@link Option} components it wraps can have access to it.
+     *
+     **/
+
+    private final static String ATTRIBUTE_NAME = "org.apache.tapestry.active.Select";
+
+    /**
+     * @see org.apache.tapestry.AbstractComponent#renderComponent(IMarkupWriter, IRequestCycle)
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        if (cycle.getAttribute(ATTRIBUTE_NAME) != null)
+            throw new ApplicationRuntimeException(
+                Tapestry.getMessage("Select.may-not-nest"),
+                this,
+                null,
+                null);
+
+        cycle.setAttribute(ATTRIBUTE_NAME, this);
+
+        boolean render = !cycle.isRewinding();
+
+        if (render)
+        {
+            writer.begin("select");
+
+            writer.attribute("name", getName());
+
+            String value = getValue();
+            if (Tapestry.isNonBlank(value))
+                writer.attribute("value", value);
+
+            String title = getTitle();
+            if (Tapestry.isNonBlank(title))
+                writer.attribute("title", title);
+
+            boolean multiple = isMultiple();
+            if (multiple)
+                writer.attribute("multiple", multiple);
+
+            renderInformalParameters(writer, cycle);
+        }
+
+        renderBody(writer, cycle);
+
+        if (render)
+        {
+            writer.end();
+        }
+
+        cycle.removeAttribute(ATTRIBUTE_NAME);
+    }
+
+    public abstract boolean isMultiple();
+
+    public abstract String getName();
+
+    public abstract String getValue();
+
+    public abstract String getTitle();
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/Select.jwc b/tapestry-framework/src/org/apache/tapestry/wml/Select.jwc
new file mode 100644
index 0000000..c9f6041
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/Select.jwc
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<component-specification class="org.apache.tapestry.wml.Select">
+
+	<description>
+		The WMLSelect element lets users pick from a list of options. Each option is specified 
+		by an Option element. Each Option element may have one line of formatted text 
+		(which may be wrapped or truncated by the user agent if too long). 
+		Option elements may be organised into hierarchical groups using the OptionGroup 
+		element. 
+	</description>
+
+	<parameter name="multiple" type="boolean" direction="in">
+        <description>
+            This attribute indicates that the select list should accept multiple selections. 
+            When not set, the select list should only accept a single selected option.
+        </description>
+	</parameter>
+
+	<parameter name="title" type="java.lang.String" direction="in">
+		<description>
+			This attribute specifies a title for this element, which may be used in the 
+			presentation of this object.
+			This attribute specifies a title for this element, which may be used in the 
+			presentation of this object.
+		</description>
+	</parameter>
+
+	<parameter name="name" type="java.lang.String" direction="in">
+        <description>
+            The name attribute indicates the name of the variable to set with the result 
+            of the selection. The variable is set to the string value of the chosen option
+            element, which is specified with the value attribute. The name variable's value 
+            is used to pre-select options in the select list.
+        </description>
+    </parameter>
+
+	<parameter name="value" type="java.lang.String" direction="in">
+        <description>
+            The value attribute indicates the default value of the variable named in the 
+            name attribute. When the element is displayed, and the variable named in the 
+            name attribute is not set, the name variable may be assigned the value 
+            specified in the value attribute, depending on the values defined in iname and 
+            ivalue. If the name variable already contains a value, the value attribute is 
+            ignored. Any application of the default value is done before the list is 
+            pre-selected with the value of the name variable.
+            If this element allows the selection of multiple options, the result of the 
+            user's choice is a list of all selected values, separated by the semicolon 
+            character. The name variable is set with this result. In addition, the value 
+            attribute is interpreted as a semicolon-separated list of pre-selected options.
+        </description>
+	</parameter>
+
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/SelectionField.java b/tapestry-framework/src/org/apache/tapestry/wml/SelectionField.java
new file mode 100644
index 0000000..d2fc1c9
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/SelectionField.java
@@ -0,0 +1,40 @@
+//  Copyright 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.tapestry.wml;
+
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.form.IPropertySelectionModel;
+
+/**
+ *  SelectionField specifies a postfield element and it is used to complement the {@link PropertySelection} component.
+ *
+ *  @version $Id$
+ *  @author David Solis
+ *  @since 3.0
+ *
+ **/
+public abstract class SelectionField extends AbstractPostfield
+{
+    protected void rewind(IRequestCycle cycle)
+    {
+        String optionValue = cycle.getRequestContext().getParameter(getName());
+        IPropertySelectionModel model = getModel();
+        Object value = (optionValue == null) ? null : model.translateValue(optionValue);
+
+        updateValue(value);
+    }
+
+    public abstract IPropertySelectionModel getModel();
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/SelectionField.jwc b/tapestry-framework/src/org/apache/tapestry/wml/SelectionField.jwc
new file mode 100644
index 0000000..0f35faa
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/SelectionField.jwc
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN"
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.wml.SelectionField" allow-body="no">
+
+    <description>
+        SelectionField specifies a postfield element and it is used to complement the PropertySelection component.
+    </description>
+
+    <parameter name="value" required="yes" type="java.lang.Object"/>
+
+    <parameter name="model"
+        type="org.apache.tapestry.form.IPropertySelectionModel"
+        required="yes"
+        direction="auto"/>
+
+    <parameter name="name" property-name="varName" type="java.lang.String" direction="in" required="yes">
+        <description>
+            The name attribute specifies an WML variable name.
+        </description>
+    </parameter>
+
+    <reserved-parameter name="value"/>
+
+    <property-specification name="name" type="java.lang.String"/>
+    <property-specification name="form" type="org.apache.tapestry.IForm"/>
+
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/Setvar.java b/tapestry-framework/src/org/apache/tapestry/wml/Setvar.java
new file mode 100644
index 0000000..57b300e
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/Setvar.java
@@ -0,0 +1,69 @@
+//  Copyright 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.tapestry.wml;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  The setvar element specifies the variable to set in the current browser context as a side effect of executing a task.
+ *
+ *  @version $Id$
+ *  @author David Solis
+ *  @since 3.0
+ *
+ */
+
+public abstract class Setvar extends AbstractComponent
+{
+    /**
+     *  @see AbstractComponent#renderComponent(IMarkupWriter, IRequestCycle)
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        boolean render = !cycle.isRewinding();
+
+        if (render)
+        {
+            writer.beginEmpty("setvar");
+
+            writer.attribute("name", getName());
+
+            renderInformalParameters(writer, cycle);
+
+            String value = readValue();
+            if (Tapestry.isNonBlank(value))
+                writer.attribute("value", value);
+            else
+                writer.attribute("value", "");
+
+            writer.closeTag();
+        }
+    }
+
+    public abstract String getName();
+
+    public abstract IBinding getValueBinding();
+
+    public String readValue()
+    {
+        return getValueBinding().getString();
+    }
+
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/Setvar.jwc b/tapestry-framework/src/org/apache/tapestry/wml/Setvar.jwc
new file mode 100644
index 0000000..eb17b20
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/Setvar.jwc
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.wml.Setvar" allow-body="no">
+
+    <description>
+        The setvar element specifies the variable to set in the current browser context as a side effect of executing
+        a task.
+    </description>
+    <parameter name="name" type="java.lang.String" direction="in" required="yes">
+        <description>
+            The name attribute specifies a WML variable name.
+        </description>
+    </parameter>
+
+    <parameter name="value" type="java.lang.String" required="yes">
+        <description>
+            Bind value to the variable that should recieve the user input string.
+        </description>
+    </parameter>
+
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/Timer.java b/tapestry-framework/src/org/apache/tapestry/wml/Timer.java
new file mode 100644
index 0000000..0bba5a7
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/Timer.java
@@ -0,0 +1,68 @@
+//  Copyright 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.tapestry.wml;
+
+import org.apache.tapestry.AbstractComponent;
+import org.apache.tapestry.IBinding;
+import org.apache.tapestry.IMarkupWriter;
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+
+/**
+ *  The Timer element declares a card timer, which exposes a means of processing inactivity or idle time.
+ *  The timer is initialised and started at card entry and is stopped when the card is exited.
+ *
+ *  @version $Id$
+ *  @author David Solis
+ *  @since 3.0
+ */
+
+public abstract class Timer extends AbstractComponent
+{
+    /**
+     *  @see AbstractComponent#renderComponent(IMarkupWriter, IRequestCycle)
+     **/
+
+    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
+    {
+        boolean render = !cycle.isRewinding();
+
+        if (render)
+        {
+            writer.beginEmpty("timer");
+
+            writer.attribute("name", getName());
+
+            String value = readValue();
+            if (Tapestry.isNonBlank(value))
+                writer.attribute("value", value);
+            else
+                writer.attribute("value", "0");
+
+            renderInformalParameters(writer, cycle);
+
+            writer.closeTag();
+        }
+    }
+
+    public abstract String getName();
+
+    public abstract IBinding getValueBinding();
+
+    public String readValue()
+    {
+        return getValueBinding().getString();
+    }
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/Timer.jwc b/tapestry-framework/src/org/apache/tapestry/wml/Timer.jwc
new file mode 100644
index 0000000..3a67805
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/Timer.jwc
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE component-specification PUBLIC
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<component-specification class="org.apache.tapestry.wml.Timer" allow-body="no">
+
+    <description>
+        The Timer element declares a card timer, which exposes a means of processing inactivity or idle time.
+        The timer is initialised and started at card entry and is stopped when the card is exited.
+    </description>
+
+    <parameter name="name" type="java.lang.String" direction="in">
+        <description>
+            The name attribute specifies the name of the variable to be set with the value of the timer. The name
+            variable's value is used to set the timeout period upon timer initialisation. The variable named by the
+            name attribute will be set with the current timer value when the card is exited or when the timer expires.
+            For example, if the timer expires, the name variable is set to a value of "0".
+        </description>
+    </parameter>
+
+    <parameter name="value" type="java.lang.String">
+        <description>
+            The value attribute indicates the default value of the variable named in the name attribute. When the
+            timer is initialised and the variable named in the name attribute is not set, the name variable is assigned
+            the value specified in the value attribute. If the name variable already contains a value, the value
+            attribute is ignored. If the name attribute is not specified, the timeout is always initialised to the
+            value specified in the value attribute.
+        </description>
+    </parameter>
+
+</component-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/WML.library b/tapestry-framework/src/org/apache/tapestry/wml/WML.library
new file mode 100644
index 0000000..4acc557
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/WML.library
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE library-specification PUBLIC
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN"
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+
+<library-specification>
+    <property name="org.apache.tapestry.template-extension" value="wml"/>
+
+    <component-type type="Card" specification-path="/org/apache/tapestry/wml/Card.jwc"/>
+    <component-type type="Do" specification-path="/org/apache/tapestry/wml/Do.jwc"/>
+    <component-type type="Go" specification-path="/org/apache/tapestry/wml/Go.jwc"/>
+    <component-type type="Input" specification-path="/org/apache/tapestry/wml/Input.jwc"/>
+    <component-type type="OnEvent" specification-path="/org/apache/tapestry/wml/OnEvent.jwc"/>
+    <component-type type="Postfield" specification-path="/org/apache/tapestry/wml/Postfield.jwc"/>
+    <component-type type="Setvar" specification-path="/org/apache/tapestry/wml/Setvar.jwc"/>
+    <component-type type="Timer" specification-path="/org/apache/tapestry/wml/Timer.jwc"/>
+    <component-type type="Image" specification-path="/org/apache/tapestry/wml/Image.jwc"/>
+    <component-type type="Option" specification-path="/org/apache/tapestry/wml/Option.jwc"/>
+    <component-type type="PropertySelection" specification-path="/org/apache/tapestry/wml/PropertySelection.jwc"/>
+    <component-type type="Select" specification-path="/org/apache/tapestry/wml/Select.jwc"/>
+    <component-type type="SelectionField" specification-path="/org/apache/tapestry/wml/SelectionField.jwc"/>
+
+</library-specification>
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/WMLEngine.java b/tapestry-framework/src/org/apache/tapestry/wml/WMLEngine.java
new file mode 100644
index 0000000..5e51101
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/WMLEngine.java
@@ -0,0 +1,89 @@
+//  Copyright 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.tapestry.wml;
+
+import javax.servlet.ServletException;
+
+import org.apache.tapestry.IRequestCycle;
+import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.engine.BaseEngine;
+
+/**
+ *  Subclass of {@link BaseEngine} used for WML applications to change the
+ *  Exception, StaleLink and StaleSession pages.
+ *
+ *  @author David Solis
+ *  @version $Id$
+ *  @since 3.0
+ * 
+ **/
+
+public class WMLEngine extends BaseEngine
+{
+	protected void activateExceptionPage(
+		IRequestCycle cycle,
+		org.apache.tapestry.request.ResponseOutputStream output,
+		Throwable cause)
+		throws ServletException
+	{
+		super.activateExceptionPage(cycle, output, cause);
+		// Sometimes the exception page isn't enough
+        reportException(
+            Tapestry.getMessage("AbstractEngine.unable-to-process-client-request"),
+            cause);
+	}
+
+
+	/** @since 3.0 **/
+
+	protected String getExceptionPageName()
+	{
+		return EXCEPTION_PAGE;
+	}
+	
+	/** @since 3.0 **/
+
+	protected String getStaleLinkPageName()
+	{
+		return STALE_LINK_PAGE;
+	}
+
+	/** @since 3.0 **/
+
+	protected String getStaleSessionPageName()
+	{
+		return STALE_SESSION_PAGE;
+	}
+
+	/**
+	 *  The name of the page used for reporting exceptions.
+	 *  
+	 **/
+	private static final String EXCEPTION_PAGE = "WMLException";
+
+	/**
+	 *  The name of the page used for reporting stale links.
+	 *
+	 * */
+
+	private static final String STALE_LINK_PAGE = "WMLStaleLink";
+
+	/**
+	 *  The name of the page used for reporting state sessions.
+	 *
+	 **/
+
+	private static final String STALE_SESSION_PAGE = "WMLStaleSession";
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/WMLWriter.java b/tapestry-framework/src/org/apache/tapestry/wml/WMLWriter.java
new file mode 100644
index 0000000..53e0398
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/WMLWriter.java
@@ -0,0 +1,116 @@
+//  Copyright 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.tapestry.wml;
+
+import java.io.OutputStream;
+
+import org.apache.tapestry.AbstractMarkupWriter;
+import org.apache.tapestry.IMarkupWriter;
+
+/**
+ *  This class is used to create WML output.
+ *
+ *  <p>The <code>WMLResponseWriter</code> handles the necessary escaping 
+ *  of invalid characters.
+ *  Specifically, the '$', '&lt;', '&gt;' and '&amp;' characters are properly
+ *  converted to their WML entities by the <code>print()</code> methods.
+ *  Similar measures are taken by the {@link #attribute(String, String)} method.
+ *  Other invalid characters are converted to their numeric entity equivalent.
+ *
+ *  <p>This class makes it easy to generate trivial and non-trivial WML pages.
+ *  It is also useful to generate WML snippets. It's ability to do simple
+ *  formatting is very useful. A JSP may create an instance of the class
+ *  and use it as an alternative to the simple-minded <b>&lt;%= ... %&gt;</b>
+ *  construct, espcially because it can handle null more cleanly.
+ *
+ *  @version $Id$
+ *  @author David Solis
+ *  @since 0.2.9
+ * 
+ **/
+
+public class WMLWriter extends AbstractMarkupWriter
+{
+
+    private static final String[] entities = new String[64];
+    private static final boolean[] safe = new boolean[128];
+    private static final String SAFE_CHARACTERS =
+        "01234567890"
+            + "abcdefghijklmnopqrstuvwxyz"
+            + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+            + "\t\n\r !\"#%'()*+,-./:;=?@[\\]^_`{|}~";
+
+    static {
+        entities['"'] = "&quot;";
+        entities['<'] = "&lt;";
+        entities['>'] = "&gt;";
+        entities['&'] = "&amp;";
+        entities['$'] = "$$";
+
+        int length = SAFE_CHARACTERS.length();
+        for (int i = 0; i < length; i++)
+            safe[SAFE_CHARACTERS.charAt(i)] = true;
+    }
+
+    /**
+     *  Creates a response writer for content type "text/vnd.wap.wml".
+     * 
+     **/
+
+    public WMLWriter(OutputStream stream)
+    {
+        this(stream, "UTF-8");
+    }
+
+    /**
+     * 
+     * @param stream the output stream where to write the text
+     * @param encoding the encoding to be used to generate the output
+     * @since 3.0
+     * 
+     **/
+    public WMLWriter(OutputStream stream, String encoding)
+    {
+        this("text/vnd.wap.wml", encoding, stream);
+    }
+
+    public WMLWriter(String contentType, OutputStream stream)
+    {
+        super(safe, entities, contentType, stream);
+    }
+
+    /**
+     * 
+     * @param mimeType the MIME type to be used to generate the content type
+     * @param encoding the encoding to be used to generate the output
+     * @param stream the output stream where to write the text
+     * @since 3.0
+     * 
+     **/
+    public WMLWriter(String mimeType, String encoding, OutputStream stream)
+    {
+        super(safe, entities, mimeType, encoding, stream);
+    }
+
+    protected WMLWriter(String contentType)
+    {
+        super(safe, entities, contentType);
+    }
+
+    public IMarkupWriter getNestedWriter()
+    {
+        return new NestedWMLWriter(this);
+    }
+}
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/package.html b/tapestry-framework/src/org/apache/tapestry/wml/package.html
new file mode 100644
index 0000000..3575716
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/package.html
@@ -0,0 +1,23 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<!-- $Id$ -->
+<html>
+<head>
+<title>Tapestry: Web Application Framework</title>
+</head>
+<body>
+
+<p>Classes and components for main elements of the Wireless Markup Language (WML 1.2).
+
+<ul>
+<li>{@link org.apache.tapestry.wml.Deck} is an alternative to
+{@link org.apache.tapestry.html.BasePage} that provides a <code>text/wml</code> response.</li>
+<li>Basic support for WMLScript.</li>
+<li><b>No support for WML style tables.</li>
+</ul>
+ 
+@author Howard Lewis Ship <a href="mailto:hlship@apache.org">hlship@apache.org</a>
+@author David Solis <a href="mailto:dsolis@apache.org">dsolis@apache.org</a>
+ 
+</body>
+</html>
+ 
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLException.java b/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLException.java
new file mode 100644
index 0000000..28418ab
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLException.java
@@ -0,0 +1,51 @@
+//  Copyright 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.tapestry.wml.pages;
+
+import org.apache.tapestry.util.exception.ExceptionAnalyzer;
+import org.apache.tapestry.util.exception.ExceptionDescription;
+import org.apache.tapestry.wml.Deck;
+
+/**
+ *  Default exception reporting page for WML applications.
+ *
+ *  @author David Solis
+ *  @version $Id$
+ *  @since 3.0
+ * 
+ **/
+public class WMLException extends Deck 
+{
+	private ExceptionDescription[] _exceptions;
+
+	public void initialize()
+	{
+		_exceptions = null;
+	}
+
+	public ExceptionDescription[] getExceptions()
+	{
+		return _exceptions;
+	}
+
+	public void setException(Throwable value)
+	{
+		ExceptionAnalyzer analyzer;
+
+		analyzer = new ExceptionAnalyzer();
+
+		_exceptions = analyzer.analyze(value);
+	}
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLException.page b/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLException.page
new file mode 100644
index 0000000..964fdd8
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLException.page
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE page-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<page-specification class="org.apache.tapestry.wml.pages.WMLException">
+  
+  <property name="org.apache.tapestry.template-extension" value="wml"/>
+  
+  <property-specification name="current" type="org.apache.tapestry.util.exception.ExceptionDescription"/>
+
+  <component id="restart" type="ServiceLink">
+    <binding name="service" expression="@org.apache.tapestry.Tapestry@RESTART_SERVICE"/>
+  </component>
+
+  <component id="foreachProperty" type="Foreach">
+  	<static-binding name="element" value="tr"/>
+    <binding name="source" expression="current.properties"/>
+  </component>
+
+</page-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLException.wml b/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLException.wml
new file mode 100644
index 0000000..2d4a7b2
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLException.wml
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<!-- $Id -->
+   <!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.2//EN"
+   "http://www.wapforum.org/DTD/wml12.dtd">
+<wml>
+    <head>
+        <meta http-equiv="Cache-Control" content="must-revalidate" forua="true"/>
+        <meta http-equiv="Cache-Control" content="no-cache" forua="true"/>
+    </head>
+
+	<card id="Exception" title="Exception">
+		<p>
+			An exception has occured.
+            You may continue by <b><a jwcid="restart">restarting</a></b> the session.<br/>
+            <table columns="1">
+				<p jwcid="@Foreach" source="ognl:exceptions" value="ognl:current">
+					<tr><td><b jwcid="@Insert" value="ognl:current.exceptionClassName">some.exception.Class</b></td></tr>
+					<tr><td><b jwcid="@Insert" value="ognl:current.message">A message describing the exception.</b></td></tr>
+					<tr jwcid="foreachProperty">
+						<td><b jwcid="@Insert" value="ognl:components.foreachProperty.value.name">Property Name</b>:<b jwcid="@Insert" value="ognl:components.foreachProperty.value.value">Property Value</b></td>
+					</tr>
+				</p>
+			</table>
+		</p>
+				
+	</card>
+</wml>
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLStaleLink.java b/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLStaleLink.java
new file mode 100644
index 0000000..61b8ebb
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLStaleLink.java
@@ -0,0 +1,33 @@
+//  Copyright 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.tapestry.wml.pages;
+
+import org.apache.tapestry.wml.Deck;
+
+
+/**
+ *  Stores a message (taken from the {@link org.apache.tapestry.StaleLinkException})
+ *  that is displayed as part of the page.
+ *
+ *  @author David Solis
+ *  @version $Id$
+ *  @since 3.0
+ *
+ **/
+
+public abstract class WMLStaleLink extends Deck
+{
+    public abstract void setMessage(String message);
+}
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLStaleLink.page b/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLStaleLink.page
new file mode 100644
index 0000000..0ee9d63
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLStaleLink.page
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE page-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+  
+<page-specification class="org.apache.tapestry.wml.pages.WMLStaleLink">
+  
+  <property name="org.apache.tapestry.template-extension" value="wml"/>
+
+  <property-specification name="message" type="java.lang.String"/>
+
+  <component id="home" type="ServiceLink">
+    <binding name="service" expression="@org.apache.tapestry.Tapestry@HOME_SERVICE"/>
+  </component>
+  
+</page-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLStaleLink.wml b/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLStaleLink.wml
new file mode 100644
index 0000000..9adb5e2
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLStaleLink.wml
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<!-- $Id -->
+   <!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.2//EN"
+   "http://www.wapforum.org/DTD/wml12.dtd">
+<wml>
+    <head>
+        <meta http-equiv="Cache-Control" content="must-revalidate" forua="true"/>
+        <meta http-equiv="Cache-Control" content="no-cache" forua="true"/>
+    </head>
+
+	<card id="StaleLink" title="StaleLink">
+		<p>You have clicked on a <i>stale link</i>.</p>  
+		<p>
+			<b jwcid="@Insert" value="ognl:message">Exception message goes here.</b>
+		</p>
+		<p>
+			This is most likely the result of using your browser's <b>back</b> button, but can also be an application error.
+		</p>
+		<p>
+			You may continue by returning to the application's <b><a jwcid="home">home page</a></b>.
+		</p>				
+	</card>
+</wml>
\ No newline at end of file
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLStaleSession.page b/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLStaleSession.page
new file mode 100644
index 0000000..718feb5
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLStaleSession.page
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 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.
+-->
+<!-- $Id$ -->
+<!DOCTYPE page-specification PUBLIC 
+  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
+  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
+	
+<page-specification class="org.apache.tapestry.wml.Deck">
+  
+  <property name="org.apache.tapestry.template-extension" value="wml"/>
+
+  <component id="restart" type="ServiceLink">
+    <binding name="service" expression="@org.apache.tapestry.Tapestry@RESTART_SERVICE"/>
+  </component>
+  
+</page-specification>
diff --git a/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLStaleSession.wml b/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLStaleSession.wml
new file mode 100644
index 0000000..6869804
--- /dev/null
+++ b/tapestry-framework/src/org/apache/tapestry/wml/pages/WMLStaleSession.wml
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<!-- $Id -->
+   <!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.2//EN"
+   "http://www.wapforum.org/DTD/wml12.dtd">
+<wml>
+    <head>
+        <meta http-equiv="Cache-Control" content="must-revalidate" forua="true"/>
+        <meta http-equiv="Cache-Control" content="no-cache" forua="true"/>
+    </head>
+
+	<card id="StaleSession" title="StaleSession">
+		<p>Your session has timed out.</p>			
+		<p>Web applications store information about what you are doing on the server.  This information
+		is called the <em>session</em>.</p>
+		
+		<p>Web servers must track many, many sessions.  If you
+		are inactive for a long enough time (usually, a few minutes), this information is discarded to
+		make room for active users.</p>
+		
+		<p>At this point you may <b><a jwcid="restart">restart</a></b> the session to continue.</p>
+	</card>
+</wml>
\ No newline at end of file