Author: sylvain Date: Mon Oct 4 02:26:14 2004 New Revision: 51874 Added: cocoon/trunk/src/blocks/forms/java/org/apache/cocoon/forms/generation/JXMacrosHelper.java (contents, props changed) cocoon/trunk/src/blocks/forms/java/org/apache/cocoon/forms/generation/jx-macros.xml (contents, props changed) Modified: cocoon/trunk/src/blocks/forms/java/org/apache/cocoon/forms/formmodel/AbstractWidget.java cocoon/trunk/src/blocks/forms/samples/flow/forms_flow_example.js cocoon/trunk/src/blocks/forms/samples/forms/form1_template.xml cocoon/trunk/src/blocks/forms/samples/sitemap.xmap Log: Implementation of the CForms template language as JX macros
Modified: cocoon/trunk/src/blocks/forms/java/org/apache/cocoon/forms/formmodel/AbstractWidget.java ============================================================================== --- cocoon/trunk/src/blocks/forms/java/org/apache/cocoon/forms/formmodel/AbstractWidget.java (original) +++ cocoon/trunk/src/blocks/forms/java/org/apache/cocoon/forms/formmodel/AbstractWidget.java Mon Oct 4 02:26:14 2004 @@ -171,8 +171,8 @@ relativeWidget = getForm(); relativePath = path.substring(1); } else { - if (path.startsWith(".." + Widget.PATH_SEPARATOR)) { - relativeWidget = getParent(); + if (path.startsWith(".." + Widget.PATH_SEPARATOR)) { + relativeWidget = getParent(); relativePath = path.substring(3); } else { String childId = path.substring(0, sepPosition ); @@ -452,5 +452,14 @@ if (this.attributes != null) { this.attributes.remove(name); } + } + + public String toString() { + String className = this.getClass().getName(); + int last = className.lastIndexOf('.'); + if (last != -1) { + className = className.substring(last+1); + } + return className + "@" + getRequestParameterName(); } } Added: cocoon/trunk/src/blocks/forms/java/org/apache/cocoon/forms/generation/JXMacrosHelper.java ============================================================================== --- (empty file) +++ cocoon/trunk/src/blocks/forms/java/org/apache/cocoon/forms/generation/JXMacrosHelper.java Mon Oct 4 02:26:14 2004 @@ -0,0 +1,224 @@ +package org.apache.cocoon.forms.generation; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; + +import org.apache.cocoon.forms.Constants; +import org.apache.cocoon.forms.formmodel.ContainerWidget; +import org.apache.cocoon.forms.formmodel.Form; +import org.apache.cocoon.forms.formmodel.Repeater; +import org.apache.cocoon.forms.formmodel.Widget; +import org.apache.cocoon.forms.validation.ValidationError; +import org.apache.cocoon.xml.AbstractXMLPipe; +import org.apache.cocoon.xml.AttributesImpl; +import org.apache.cocoon.xml.XMLConsumer; +import org.apache.commons.collections.ArrayStack; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; + +/** + * Helper class for the implementation of the CForms template language with JXTemplate macros. + * + * @version CVS $Id$ + */ +public class JXMacrosHelper { + + private XMLConsumer cocoonConsumer; + private ArrayStack stack = new ArrayStack(); + + private Map classes = null; // lazily created + + /** + * Builds and helper object, given the generator's consumer. + * + * @param consumer the generator's consumer + * @return a helper object + */ + public static JXMacrosHelper createHelper(XMLConsumer consumer) { + return new JXMacrosHelper(consumer); + } + + public JXMacrosHelper(XMLConsumer consumer) { + this.cocoonConsumer = consumer; + } + + public void startForm(Map attributes) throws SAXException { + // build attributes + AttributesImpl attrs = new AttributesImpl(); + Iterator iter = attributes.entrySet().iterator(); + while(iter.hasNext()) { + Map.Entry entry = (Map.Entry)iter.next(); + attrs.addCDATAAttribute((String)entry.getKey(), (String)entry.getValue()); + } + + this.cocoonConsumer.startPrefixMapping(Constants.INSTANCE_PREFIX, Constants.INSTANCE_NS); + this.cocoonConsumer.startElement( + Constants.INSTANCE_NS, + "form-template", + Constants.INSTANCE_PREFIX_COLON + "form-template", + attrs); + } + + public void endForm() throws SAXException { + this.cocoonConsumer.endElement( + Constants.INSTANCE_NS, + "form-template", + Constants.INSTANCE_PREFIX_COLON + "form-template"); + this.cocoonConsumer.endPrefixMapping(Constants.INSTANCE_PREFIX); + } + + /** + * Flush the root element name that has been stored in [EMAIL PROTECTED] #generateSaxFragment(Widget, Locale)} + * + * @param obj the object that is terminated (widget or validation error) + * @throws SAXException + */ + public void flushRoot(Object obj) throws SAXException { + Object stackObj = stack.pop(); + if (stackObj != obj) { + throw new IllegalStateException("Flushing on wrong widget (expected " + stackObj + + ", got " + obj + ")"); + } + ((RootBufferingPipe)stack.pop()).flushRoot(); + } + + /** + * Get a child widget of a given widget, throwing an exception if no such child exists. + * + * @param currentWidget + * @param id + * @return + */ + public Widget getWidget(Widget currentWidget, String id) { + Widget result = null; + + if (currentWidget instanceof ContainerWidget) { + result = ((ContainerWidget)currentWidget).getChild(id); + } + + if (result != null) { + return result; + } else { + throw new IllegalArgumentException("Widget '" + currentWidget + + "' has no child named '" + id + "'"); + } + } + + public Repeater getRepeater(Widget currentWidget, String id) { + Widget child = getWidget(currentWidget, id); + if (child instanceof Repeater) { + return (Repeater)child; + } else { + throw new IllegalArgumentException("Widget '" + child + "' is not a repeater"); + } + } + + /** + * Generate a widget's SAX fragment, buffering the root element's <code>endElement()</code> + * event so that the template can insert styling information in it. + * + * @param widget + * @param locale + * @throws SAXException + */ + public void generateWidget(Widget widget, Locale locale) throws SAXException { + // Needs to be buffered + RootBufferingPipe pipe = new RootBufferingPipe(this.cocoonConsumer); + this.stack.push(pipe); + this.stack.push(widget); + widget.generateSaxFragment(pipe, locale); + } + + public void generateWidgetLabel(Widget widget, String id) throws SAXException { + getWidget(widget, id).generateLabel(this.cocoonConsumer); + } + + public void generateRepeaterWidgetLabel(Widget widget, String id, String widgetId) throws SAXException { + getRepeater(widget, id).generateWidgetLabel(widgetId, this.cocoonConsumer); + } + + public void generateRepeaterSize(Widget widget, String id) throws SAXException { + getRepeater(widget, id).generateSize(this.cocoonConsumer); + } + + public void generateValidationError(ValidationError error) throws SAXException { + // Needs to be buffered + RootBufferingPipe pipe = new RootBufferingPipe(this.cocoonConsumer); + this.stack.push(pipe); + this.stack.push(error); + error.generateSaxFragment(pipe); + } + + public void defineClassBody(Form form, String id, Object body) { + // TODO: check that class actually exists in the form + if (this.classes == null) { + this.classes = new HashMap(); + } + + // TODO: check if class doesn't already exist? + this.classes.put(id, body); + } + + public Object getClassBody(String id) { + Object result = this.classes == null ? null : this.classes.get(id); + + if (result == null) { + throw new IllegalArgumentException("No class '" + id + "' has been defined."); + } else { + return result; + } + } + + public boolean isSelectedCase(Widget unionWidget, String caseValue) { + String value = (String)unionWidget.getValue(); + return caseValue.equals(value != null ? value : ""); + } + + /** + * A SAX pipe that buffers the <code>endElement()</code> event of the root element. + * This is needed by the generator version of the Woody transformer (see woody-jxmacros.xml). + * + * @version CVS $Id$ + */ + private static class RootBufferingPipe extends AbstractXMLPipe { + private int depth = 0; + + private String rootUri; + private String rootLoc; + private String rootRaw; + + public RootBufferingPipe(XMLConsumer next) { + this.setConsumer(next); + } + + public void startElement(String uri, String loc, String raw, Attributes a) throws SAXException + { + if (depth == 0) { + // Root element: keep its description + this.rootUri = uri; + this.rootLoc = loc; + this.rootRaw = raw; + } + depth++; + super.startElement(uri, loc, raw, a); + } + + public void endElement(String uri, String loc, String raw) throws SAXException + { + depth--; + if (depth > 0) { + // Propagate all but root element + super.endElement(uri, loc, raw); + } + } + + public void flushRoot() throws SAXException { + if (depth != 0) { + throw new IllegalStateException("Depth is not zero"); + } + super.endElement(this.rootUri, this.rootLoc, this.rootRaw); + } + } +} Added: cocoon/trunk/src/blocks/forms/java/org/apache/cocoon/forms/generation/jx-macros.xml ============================================================================== --- (empty file) +++ cocoon/trunk/src/blocks/forms/java/org/apache/cocoon/forms/generation/jx-macros.xml Mon Oct 4 02:26:14 2004 @@ -0,0 +1,152 @@ +<!-- An implementation of the CForms template engine as a JXTemplate tag library --> + +<jx:template xmlns:jx="http://apache.org/cocoon/templates/jx/1.0" + xmlns:fi="http://apache.org/cocoon/forms/1.0#instance"> + <!-- + ft:form-template + --> + <jx:macro name="form-template" targetNamespace="http://apache.org/cocoon/forms/1.0#template"> + <jx:set var="cformsHelper" value="${Packages.org.apache.cocoon.forms.generation.JXMacrosHelper.createHelper(cocoon.consumer)}"/> + <jx:set var="cformsDummy" value="${cformsHelper.startForm(macro.arguments)}"/> + <jx:set var="form" value="${CocoonFormsInstance}"/> + <!-- the form is also the current widget --> + <jx:set var="widget" value="${form}"/> + <jx:evalBody/> + <jx:set var="cformsDummy" value="${cformsHelper.endForm()}"/> + </jx:macro> + + <!-- + ft:widget + --> + <jx:macro name="widget" targetNamespace="http://apache.org/cocoon/forms/1.0#template"> + <jx:parameter name="id"/> + + <jx:set var="widget" value="${cformsHelper.getWidget(widget, id)}"/> + <jx:set var="cformsDummy" value="${cformsHelper.generateWidget(widget, locale)}"/> + <jx:evalBody/> + <jx:set var="cformsDummy" value="${cformsHelper.flushRoot(widget)}"/> + </jx:macro> + + <!-- + ft:repeater-widget-label + --> + <jx:macro name="repeater-widget-label" targetNamespace="http://apache.org/cocoon/forms/1.0#template"> + <jx:parameter name="id"/> + <jx:parameter name="widget-id"/> + + <jx:set var="cformsDummy" value="${cformsHelper.generateRepeaterWidgetLabel(widget, id, this['widget-id'])}"/> + </jx:macro> + + <!-- + ft:widget-label + --> + <jx:macro name="widget-label" targetNamespace="http://apache.org/cocoon/forms/1.0#template"> + <jx:parameter name="id"/> + + <jx:set var="cformsDummy" value="${cformsHelper.generateWidgetLabel(widget, id)}"/> + </jx:macro> + + <!-- + ft:repeater-size + --> + <jx:macro name="repeater-size" targetNamespace="http://apache.org/cocoon/forms/1.0#template"> + <jx:parameter name="id"/> + + <jx:set var="cformsDummy" value="${cformsHelper.generateRepeaterSize(widget, id)}"/> + </jx:macro> + + <!-- + ft:repeater-widget + --> + <jx:macro name="repeater-widget" targetNamespace="http://apache.org/cocoon/forms/1.0#template"> + <jx:parameter name="id"/> + + <jx:set var="repeater" value="${cformsHelper.getRepeater(widget, id)}"/> + <jx:forEach varStatus="repeaterLoop" begin="0" end="${repeater.getSize() - 1}"> + <jx:set var="widget" value="${repeater.getRow(repeaterLoop.index)}"/> + <jx:evalBody/> + </jx:forEach> + </jx:macro> + + <!-- + ft:continuation-id + --> + <jx:macro name="continuation-id" targetNamespace="http://apache.org/cocoon/forms/1.0#template"> + <fi:continuation-id>${cocoon.continuation.id}</fi:continuation-id> + </jx:macro> + + <!-- + ft:class + --> + <jx:macro name="class" targetNamespace="http://apache.org/cocoon/forms/1.0#template"> + <jx:parameter name="id"/> + + <jx:set var="cformsDummy" value="${cformsHelper.defineClassBody(form, id, macro.body)}"/> + </jx:macro> + + <!-- + ft:new + --> + <jx:macro name="new" targetNamespace="http://apache.org/cocoon/forms/1.0#template"> + <jx:parameter name="id"/> + + <jx:eval select="${cformsHelper.getClassBody(id)}"/> + </jx:macro> + + <!-- + ft:struct : just increase the nesting level + --> + <jx:macro name="struct" targetNamespace="http://apache.org/cocoon/forms/1.0#template"> + <jx:parameter name="id"/> + + <jx:set var="widget" value="${cformsHelper.getWidget(widget, id)}"/> + <fi:struct id="${widget.getRequestParameterName()}"> + <jx:evalBody/> + </fi:struct> + </jx:macro> + + <!-- + ft:union + --> + <jx:macro name="union" targetNamespace="http://apache.org/cocoon/forms/1.0#template"> + <jx:parameter name="id"/> + + <jx:set var="widget" value="${cformsHelper.getWidget(widget, id)}"/> + <fi:union id="${widget.getRequestParameterName()}"> + <jx:evalBody/> + </fi:union> + </jx:macro> + + <!-- + ft:case + --> + <jx:macro name="case" targetNamespace="http://apache.org/cocoon/forms/1.0#template"> + <jx:parameter name="id"/> + + <jx:if test="${cformsHelper.isSelectedCase(widget, id)}"> + <jx:set widget="${cformsHelper.getWidget(widget, id)}"/> + <jx:evalBody/> + </jx:if> + </jx:macro> + + <!-- + ft:validation-error + --> + <jx:macro name="validation-error" targetNamespace="http://apache.org/cocoon/forms/1.0#template"> + <jx:parameter name="id"/> + + <jx:set var="widget" value="${cformsHelper.getWidget(widget, id)}"/> + <jx:set var="validationError" value="${widget.getValidationError()}"/> + <jx:if test="${validationError != null}"> + <jx:set var="cformsDummy" value="${cformsHelper.generateValidationError(validationError)}"/> + <jx:evalBody/> + <jx:set var="cformsDummy" value="${cformsHelper.flushRoot(validationError)}"/> + </jx:if> + </jx:macro> + + + <!-- + ft:aggregate-widget : TODO + --> + +</jx:template> Modified: cocoon/trunk/src/blocks/forms/samples/flow/forms_flow_example.js ============================================================================== --- cocoon/trunk/src/blocks/forms/samples/flow/forms_flow_example.js (original) +++ cocoon/trunk/src/blocks/forms/samples/flow/forms_flow_example.js Mon Oct 4 02:26:14 2004 @@ -32,7 +32,7 @@ model.drinks = ["Jupiler", "Coca Cola"]; form.locale = locale; - form.showForm("form1-display-pipeline"); + form.showForm("form1-display-pipeline.jx"); print("submitId = " + form.submitId); if (form.isValid) { print("visa=" + model.visa); Modified: cocoon/trunk/src/blocks/forms/samples/forms/form1_template.xml ============================================================================== --- cocoon/trunk/src/blocks/forms/samples/forms/form1_template.xml (original) +++ cocoon/trunk/src/blocks/forms/samples/forms/form1_template.xml Mon Oct 4 02:26:14 2004 @@ -14,9 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. --> -<!-- The only difference between this file and the form1_template_action.xml - is the value of the action attribute on the ft:form-template element --> -<page xmlns:ft="http://apache.org/cocoon/forms/1.0#template" xmlns:fi="http://apache.org/cocoon/forms/1.0#instance"> +<!-- This file is similar to form1_template_action.xml. They differ in group layout, + form's action attribute (to use continuations) and, this one is a dynamic form + template that changes the 'contacts' repeater layout depending on its size --> +<page xmlns:ft="http://apache.org/cocoon/forms/1.0#template" + xmlns:fi="http://apache.org/cocoon/forms/1.0#instance" + xmlns:jx="http://apache.org/cocoon/templates/jx/1.0"> + + <!-- Import the macros that define CForms template elements --> + <jx:import uri="resource://org/apache/cocoon/forms/generation/jx-macros.xml"/> + <title>Sample form</title> <content> <ft:form-template action="#{$continuation/id}.continue" method="POST"> @@ -96,24 +103,34 @@ <th><ft:repeater-widget-label id="contacts" widget-id="birthdate"/></th> <th><ft:repeater-widget-label id="contacts" widget-id="select"/></th> </tr> - <!-- The contents of the repeater-widget element is a template that will - be applied to each row in the repeater. --> - <ft:repeater-widget id="contacts"> - <tr> - <td><ft:widget id="firstname"/></td> - <td><ft:widget id="lastname"/></td> - <td><ft:widget id="phone"/></td> - <td><ft:widget id="email"/></td> - <td> - <ft:widget id="birthdate"/> - </td> - <td><ft:widget id="select"/></td> - </tr> - </ft:repeater-widget> + <jx:choose> + <jx:when test="${widget.getChild('contacts').getSize() == 0}"> + <tr><td colspan="6" align="center"><em>There are no contacts to display</em></td></tr> + </jx:when> + <jx:otherwise> + <!-- The contents of the repeater-widget element is a template that will + be applied to each row in the repeater. --> + <ft:repeater-widget id="contacts"> + <tr> + <td><ft:widget id="firstname"/></td> + <td><ft:widget id="lastname"/></td> + <td><ft:widget id="phone"/></td> + <td><ft:widget id="email"/></td> + <td><ft:widget id="birthdate"/></td> + <td><ft:widget id="select"/></td> + </tr> + </ft:repeater-widget> + </jx:otherwise> + </jx:choose> <tr> - <td colspan="4" align="right"> + <td colspan="6"> <ft:widget id="addcontact"/> - <ft:widget id="removecontacts"/> + <jx:if test="${widget.getChild('contacts').getSize() > 0}"> + <ft:widget id="removecontacts"/> + <br/> + <small>Hint: remove all contacts to see how dynamic form templates can change + their layout depending on widget values</small> + </jx:if> </td> </tr> </table> Modified: cocoon/trunk/src/blocks/forms/samples/sitemap.xmap ============================================================================== --- cocoon/trunk/src/blocks/forms/samples/sitemap.xmap (original) +++ cocoon/trunk/src/blocks/forms/samples/sitemap.xmap Mon Oct 4 02:26:14 2004 @@ -153,8 +153,10 @@ <map:call continuation="{1}"/> </map:match> + <!-- + | Show a form, using the forms transformer + --> <map:match pattern="*-display-pipeline"> - <!-- pipeline to show the form --> <map:generate src="forms/{1}_template.xml"/> <map:transform type="forms"/> <map:transform type="i18n"> @@ -167,6 +169,24 @@ <map:serialize/> </map:match> + <!-- + | Show a form, using the jx template macros + --> + <map:match pattern="*-display-pipeline.jx"> + <map:generate type="jx" src="forms/{1}_template.xml"/> + <map:transform type="i18n"> + <map:parameter name="locale" value="en-US"/> + </map:transform> + <map:call resource="simple-page2html"> + <map:parameter name="file" value="forms/{1}_template.xml"/> + </map:call> + <map:transform src="resources/forms-samples-styling.xsl"/> + <map:serialize/> + </map:match> + + <!-- + | Display a success page using XSP + --> <map:match pattern="*-success-pipeline.xsp"> <map:generate type="serverpages" src="forms/{1}_success.xsp"/> <map:call resource="simple-page2html">