| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.cocoon.forms.generation; |
| |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.apache.cocoon.ajax.BrowserUpdateTransformer; |
| import org.apache.cocoon.environment.Request; |
| import org.apache.cocoon.forms.FormsConstants; |
| import org.apache.cocoon.forms.FormsRuntimeException; |
| import org.apache.cocoon.forms.event.ValueChangedListenerEnabled; |
| 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.formmodel.tree.Tree; |
| import org.apache.cocoon.forms.formmodel.tree.TreeWalker; |
| import org.apache.cocoon.forms.validation.ValidationError; |
| import org.apache.cocoon.i18n.I18nUtils; |
| import org.apache.cocoon.xml.AbstractXMLPipe; |
| import org.apache.cocoon.xml.AttributesImpl; |
| import org.apache.cocoon.xml.XMLConsumer; |
| import org.apache.cocoon.xml.XMLUtils; |
| import org.apache.commons.collections.ArrayStack; |
| import org.apache.commons.lang.BooleanUtils; |
| import org.apache.commons.lang.StringUtils; |
| import org.xml.sax.Attributes; |
| import org.xml.sax.ContentHandler; |
| import org.xml.sax.SAXException; |
| |
| /** |
| * Helper class for the implementation of the CForms template language with JXTemplate macros. |
| * |
| * @version $Id$ |
| */ |
| public class JXMacrosHelper { |
| |
| private XMLConsumer cocoonConsumer; |
| private Request request; |
| private Locale locale; |
| private ArrayStack widgetStack = new ArrayStack(); |
| private ArrayStack pipeStack = new ArrayStack(); |
| private ArrayStack labelStack = new ArrayStack(); |
| private Map classes; // lazily created |
| private boolean ajaxRequest; |
| private boolean ajaxTemplate; |
| private Set updatedWidgets; |
| private Set childUpdatedWidgets; |
| |
| /** |
| * 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, Request request, String locale) { |
| return new JXMacrosHelper(consumer, request, locale); |
| } |
| |
| public JXMacrosHelper(XMLConsumer consumer, Request request, String locale) { |
| this.cocoonConsumer = consumer; |
| this.request = request; |
| this.locale = I18nUtils.parseLocale(locale); |
| this.ajaxRequest = request.getParameter("cocoon-ajax") != null; |
| } |
| |
| public Form getForm(Form form, String attributeName) { |
| Form returnForm = form; |
| // if there hasn't been passed a form object, try to find it in the request |
| if (returnForm == null) { |
| returnForm = (Form) this.request.getAttribute(attributeName); |
| } |
| if (returnForm != null) { |
| return returnForm; |
| } |
| throw new FormsRuntimeException("The template cannot find a form object"); |
| } |
| |
| public void startForm(Form form, Map attributes) throws SAXException { |
| |
| this.updatedWidgets = form.getUpdatedWidgetIds(); |
| this.childUpdatedWidgets = form.getChildUpdatedWidgetIds(); |
| this.ajaxTemplate = "true".equals(attributes.get("ajax")); |
| |
| // build attributes |
| AttributesImpl attrs = new AttributesImpl(); |
| // top-level widget-containers like forms might have their id set to "" |
| // for those the @id should not be included. |
| if (form.getId().length() != 0) { |
| attrs.addCDATAAttribute("id", form.getRequestParameterName()); |
| } |
| |
| // Add the "state" attribute |
| attrs.addCDATAAttribute("state", form.getCombinedState().getName()); |
| |
| // Add locale attribute, useful for client-side code which needs to do stuff that |
| // corresponds to the form locale (e.g. date pickers) |
| attrs.addCDATAAttribute("locale", StringUtils.replace(this.locale.toString(), "_", "-")); |
| |
| // Add the "listening" attribute is the value has change listeners |
| if (form instanceof ValueChangedListenerEnabled && |
| ((ValueChangedListenerEnabled)form).hasValueChangedListeners()) { |
| attrs.addCDATAAttribute("listening", "true"); |
| } |
| Iterator iter = attributes.entrySet().iterator(); |
| while(iter.hasNext()) { |
| Map.Entry entry = (Map.Entry)iter.next(); |
| final String attrName = (String)entry.getKey(); |
| // check if the attribute has already been defined |
| if (attrs.getValue(attrName) != null) { |
| attrs.removeAttribute(attrName); |
| } |
| attrs.addCDATAAttribute(attrName, (String)entry.getValue()); |
| } |
| |
| // The child widgets of the form should update the label |
| this.labelStack.push(BooleanUtils.toBooleanObject(this.ajaxTemplate && this.ajaxRequest)); |
| this.cocoonConsumer.startPrefixMapping(FormsConstants.INSTANCE_PREFIX, FormsConstants.INSTANCE_NS); |
| this.cocoonConsumer.startElement(FormsConstants.INSTANCE_NS, |
| "form-template", |
| FormsConstants.INSTANCE_PREFIX_COLON + "form-template", |
| attrs); |
| // Push the form at the top of the stack |
| this.widgetStack.push(Boolean.FALSE); // Not in an updated template |
| this.widgetStack.push(form); |
| } |
| |
| public void endForm() throws SAXException { |
| this.widgetStack.pop(); |
| this.widgetStack.pop(); |
| this.labelStack.pop(); |
| this.cocoonConsumer.endElement(FormsConstants.INSTANCE_NS, |
| "form-template", |
| FormsConstants.INSTANCE_PREFIX_COLON + "form-template"); |
| this.cocoonConsumer.endPrefixMapping(FormsConstants.INSTANCE_PREFIX); |
| |
| this.ajaxTemplate = false; |
| this.updatedWidgets = null; |
| } |
| |
| private void startBuReplace(String id) throws SAXException { |
| AttributesImpl attr = new AttributesImpl(); |
| attr.addCDATAAttribute("id", id); |
| this.cocoonConsumer.startElement(BrowserUpdateTransformer.BU_NSURI, "replace", "bu:replace", attr); |
| } |
| |
| private void endBuReplace(String id) throws SAXException { |
| this.cocoonConsumer.endElement(BrowserUpdateTransformer.BU_NSURI, "replace", "bu:replace"); |
| } |
| |
| protected boolean pushWidget(String path, boolean unused) throws SAXException { |
| Widget parent = peekWidget(); |
| if (path == null || path.length() == 0) { |
| throw new FormsRuntimeException("Missing 'id' attribute on template instruction"); |
| } |
| Widget widget = parent.lookupWidget(path); |
| if (widget == null) { |
| throw new FormsRuntimeException(parent + " has no child named '" + path + "'", parent.getLocation()); |
| } |
| |
| String id = widget.getFullName(); |
| // Is there an updated widget at a higher level in the template? |
| boolean inUpdatedTemplate = ((Boolean)widgetStack.peek(1)).booleanValue(); |
| |
| boolean display; |
| |
| if (ajaxRequest) { |
| // An Ajax request. We will send partial updates |
| if (inUpdatedTemplate) { |
| // A parent widget has been updated: redisplay this one also |
| display = true; |
| } else if (this.updatedWidgets.contains(id)) { |
| // Widget has been updated. We are now in an updated template section, |
| // and widgets have to be surrounded with <bu:replace> |
| inUpdatedTemplate = true; |
| display = true; |
| } else if (this.childUpdatedWidgets.contains(id)) { |
| // A child need to be updated |
| display = true; |
| } else { |
| // Doesn't need to be displayed |
| display = false; |
| } |
| } else { |
| // Not an ajax request |
| if (ajaxTemplate) { |
| // Surround all widgets with <bu:replace>, which the bu tranformer will use to check structure |
| // consistency and add an id attribute to its child elements. |
| inUpdatedTemplate = true; |
| } |
| // Display the widget |
| display = true; |
| } |
| |
| if (display) { |
| // Widget needs to be displayed, but does it actually allows it? |
| if (widget.getState().isDisplayingValues()) { |
| if (inUpdatedTemplate) { |
| // Updated part of an Ajax template: surround with <bu:replace> |
| startBuReplace(id); |
| } |
| } else { |
| if (ajaxTemplate) { |
| // Generate a placeholder, so that the page can be updated later |
| startBuReplace(id); |
| AttributesImpl attrs = new AttributesImpl(); |
| attrs.addCDATAAttribute("id", widget.getRequestParameterName()); |
| attrs.addCDATAAttribute("state", widget.getCombinedState().getName()); |
| this.cocoonConsumer.startElement(FormsConstants.INSTANCE_NS, "placeholder", FormsConstants.INSTANCE_PREFIX_COLON + "placeholder", attrs); |
| // check if the parent wants to update the label of the child widget |
| if (ajaxRequest && ((Boolean)this.labelStack.peek()).booleanValue()) { |
| Map style = new HashMap(1); |
| style.put("update-label", "true"); |
| generateStyling(style); |
| } |
| this.cocoonConsumer.endElement(FormsConstants.INSTANCE_NS, "placeholder", FormsConstants.INSTANCE_PREFIX_COLON + "placeholder"); |
| endBuReplace(id); |
| } |
| // Production finished for this widget |
| display = false; |
| } |
| } |
| |
| if (display) { |
| this.widgetStack.push(BooleanUtils.toBooleanObject(inUpdatedTemplate)); |
| this.widgetStack.push(widget); |
| } |
| return display; |
| } |
| |
| public Widget peekWidget() { |
| return (Widget)this.widgetStack.peek(); |
| } |
| |
| public void popWidget() throws SAXException { |
| Widget widget = (Widget)this.widgetStack.pop(); |
| boolean inUpdatedTemplate = ((Boolean)this.widgetStack.pop()).booleanValue(); |
| |
| if (inUpdatedTemplate) { |
| // Close the bu:replace |
| endBuReplace(widget.getFullName()); |
| } |
| if (widget instanceof Repeater) { |
| this.labelStack.pop(); |
| } |
| } |
| |
| public boolean pushWidget(String path) throws SAXException { |
| return pushWidget(path, false); |
| } |
| |
| public boolean pushContainer(String path) throws SAXException { |
| return pushWidget(path, true); |
| } |
| |
| /** |
| * Enter a repeater |
| * |
| * @param path widget path |
| * @param ajaxAware distinguishes between <ft:repeater-widget> and <ft:repeater>. |
| * @return true if the repeater template is to be executed |
| * @throws SAXException |
| */ |
| public boolean pushRepeater(String path, boolean ajaxAware) throws SAXException { |
| if (!ajaxAware && this.ajaxTemplate) { |
| throw new IllegalStateException("Cannot use <ft:repeater-widget> in an Ajax form"); |
| } |
| boolean result = pushWidget(path, true); |
| if (result && !(peekWidget() instanceof Repeater)) { |
| throw new IllegalArgumentException("Widget " + peekWidget() + " is not a repeater"); |
| } |
| // the child widgets of the repeater never update the label |
| this.labelStack.push(Boolean.FALSE); |
| return result; |
| } |
| |
| /** |
| * Get a child widget of a given widget, throwing an exception if no such child exists. |
| * |
| * @param currentWidget |
| * @param path |
| */ |
| public Widget getWidget(Widget currentWidget, String path) { |
| Widget result = currentWidget.lookupWidget(path); |
| |
| if (result != null) { |
| return result; |
| } |
| throw new FormsRuntimeException(currentWidget + " has no child named '" + path + "'", currentWidget.getLocation()); |
| } |
| |
| private Repeater getRepeater(Widget currentWidget, String id) { |
| Widget child = getWidget(currentWidget, id); |
| if (child instanceof Repeater) { |
| return (Repeater)child; |
| } |
| throw new FormsRuntimeException(child + " is not a repeater", child.getLocation()); |
| } |
| |
| /** |
| * 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 arguments |
| * @throws SAXException |
| */ |
| public void generateWidget(Widget widget, Map arguments) throws SAXException { |
| Map args = new HashMap(arguments); |
| // check if the parent wants to update the label of the child widget |
| if (((Boolean)this.labelStack.peek()).booleanValue()) { |
| args.put("update-label", "true"); |
| } |
| // Needs to be buffered |
| RootBufferingPipe pipe = new RootBufferingPipe(this.cocoonConsumer, args); |
| this.pipeStack.push(pipe); |
| widget.generateSaxFragment(pipe, this.locale); |
| } |
| |
| /** |
| * Flush the root element name that has been stored in |
| * {@link #generateWidget(Widget, Map)}. |
| * |
| * @throws SAXException |
| */ |
| public void flushRootAndPop() throws SAXException { |
| ((RootBufferingPipe) pipeStack.pop()).flushRoot(); |
| popWidget(); |
| } |
| |
| public void flushRoot() throws SAXException { |
| ((RootBufferingPipe) pipeStack.pop()).flushRoot(); |
| } |
| |
| public void generateWidgetLabel(Widget widget, String id) throws SAXException { |
| Widget widgetInst = getWidget(widget, id); |
| if (widget instanceof Repeater) { |
| widgetInst.generateLabel(this.cocoonConsumer); |
| } else { |
| AttributesImpl attrs = new AttributesImpl(); |
| attrs.addCDATAAttribute("id", widgetInst.getRequestParameterName()); |
| attrs.addCDATAAttribute("state", widgetInst.getCombinedState().getName()); |
| this.cocoonConsumer.startElement(FormsConstants.INSTANCE_NS, "field-label", FormsConstants.INSTANCE_PREFIX_COLON + "field-label", attrs); |
| if (widgetInst.getCombinedState().isDisplayingValues()) { |
| widgetInst.getDefinition().generateDisplayData(this.cocoonConsumer); |
| } |
| this.cocoonConsumer.endElement(FormsConstants.INSTANCE_NS, "field-label", FormsConstants.INSTANCE_PREFIX_COLON + "field-label"); |
| } |
| } |
| |
| public void generateRepeaterWidgetLabel(Widget widget, String id, String widgetId) throws SAXException { |
| // Widget labels are allowed either inside or outside of <ft:repeater> |
| Repeater repeater = widget instanceof Repeater ? (Repeater)widget : getRepeater(widget, id); |
| repeater.generateWidgetLabel(widgetId, this.cocoonConsumer); |
| } |
| |
| public void generateRepeaterSize(Widget widget, String id) throws SAXException { |
| getRepeater(widget, id).generateSize(this.cocoonConsumer); |
| } |
| |
| private static final String VALIDATION_ERROR = "validation-error"; |
| |
| public void generateValidationError(ValidationError error) throws SAXException { |
| // Needs to be buffered |
| RootBufferingPipe pipe = new RootBufferingPipe(this.cocoonConsumer); |
| this.pipeStack.push(pipe); |
| pipe.startElement(FormsConstants.INSTANCE_NS, VALIDATION_ERROR, FormsConstants.INSTANCE_PREFIX_COLON + VALIDATION_ERROR, XMLUtils.EMPTY_ATTRIBUTES); |
| error.generateSaxFragment(pipe); |
| pipe.endElement(FormsConstants.INSTANCE_NS, VALIDATION_ERROR, FormsConstants.INSTANCE_PREFIX_COLON + VALIDATION_ERROR); |
| } |
| |
| public boolean isValidationError(Object object) { |
| return object instanceof ValidationError; |
| } |
| |
| 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 FormsRuntimeException("No class '" + id + "' has been defined."); |
| } |
| return result; |
| } |
| |
| public boolean isSelectedCase(Widget unionWidget, String caseValue) { |
| String value = (String)unionWidget.getValue(); |
| return caseValue.equals(value != null ? value : ""); |
| } |
| |
| public TreeWalker createWalker() { |
| return new TreeWalker((Tree)peekWidget()); |
| } |
| |
| public boolean isVisible(Widget widget) throws SAXException { |
| boolean visible = widget.getCombinedState().isDisplayingValues(); |
| |
| if (!visible) { |
| // Generate a placeholder it not visible |
| String id = widget.getRequestParameterName(); |
| AttributesImpl attrs = new AttributesImpl(); |
| attrs.addCDATAAttribute("id", id); |
| this.cocoonConsumer.startElement(BrowserUpdateTransformer.BU_NSURI, "replace", "bu:replace", attrs); |
| this.cocoonConsumer.startElement(FormsConstants.INSTANCE_NS, "placeholder", FormsConstants.INSTANCE_PREFIX_COLON + "placeholder", attrs); |
| this.cocoonConsumer.endElement(FormsConstants.INSTANCE_NS, "placeholder", FormsConstants.INSTANCE_PREFIX_COLON + "placeholder"); |
| this.cocoonConsumer.endElement(BrowserUpdateTransformer.BU_NSURI, "replace", "bu:replace"); |
| } |
| return visible; |
| } |
| |
| public boolean isModified(Widget widget) { |
| return this.updatedWidgets.contains(widget.getRequestParameterName()); |
| } |
| |
| public boolean generateStyling(Map attributes) throws SAXException { |
| return generateStyling(this.cocoonConsumer, attributes); |
| } |
| |
| /** |
| * Generate a <code><fi:styling></code> element holding the attributes of a <code>ft:*</code> |
| * element that are in the "fi:" namespace. |
| * |
| * @param attributes the template instruction attributes |
| * @return true if a <code><fi:styling></code> was produced |
| * @throws SAXException |
| */ |
| public static boolean generateStyling(ContentHandler handler, Map attributes) throws SAXException { |
| AttributesImpl attr = null; |
| Iterator entries = attributes.entrySet().iterator(); |
| while (entries.hasNext()) { |
| Map.Entry entry = (Map.Entry)entries.next(); |
| String key = (String)entry.getKey(); |
| |
| // FIXME: JXTG only gives the local name of attributes, so we can't distinguish namespaces... |
| if (!"id".equals(key) && !"widget-id".equals(key)) { |
| if (attr == null) |
| attr = new AttributesImpl(); |
| attr.addCDATAAttribute(key, (String)entry.getValue()); |
| } |
| } |
| if (attr != null) { |
| // There were some styling attributes |
| handler.startElement(FormsConstants.INSTANCE_NS, "styling", FormsConstants.INSTANCE_PREFIX_COLON + "styling", attr); |
| handler.endElement(FormsConstants.INSTANCE_NS, "styling", FormsConstants.INSTANCE_PREFIX_COLON + "styling"); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| /** |
| * A SAX pipe that buffers the <code>endElement()</code> event of the root element. |
| * This is needed by the generator version of the FormsTransformer (see jx-macros.xml). |
| * |
| * @version $Id$ |
| */ |
| private static class RootBufferingPipe extends AbstractXMLPipe { |
| private int depth = 0; |
| private String rootUri; |
| private String rootLoc; |
| private String rootRaw; |
| private Map arguments; |
| private boolean forbidStyling = false; |
| |
| public RootBufferingPipe(XMLConsumer next) { |
| this(next, Collections.EMPTY_MAP); |
| } |
| |
| public RootBufferingPipe(XMLConsumer next, Map arguments) { |
| this.setConsumer(next); |
| this.arguments = arguments; |
| } |
| |
| public void startElement(String uri, String loc, String raw, Attributes a) |
| throws SAXException { |
| super.startElement(uri, loc, raw, a); |
| if (depth == 0) { |
| // Root element: keep its description |
| this.rootUri = uri; |
| this.rootLoc = loc; |
| this.rootRaw = raw; |
| |
| // And produce fi:styling from attributes |
| this.forbidStyling = generateStyling(this.contentHandler, arguments); |
| } |
| |
| if (depth == 1 && forbidStyling && |
| uri.equals(FormsConstants.INSTANCE_NS) && loc.equals("styling")) { |
| throw new SAXException("Cannot use 'fi:*' attributes and <fi:styling> at the same time"); |
| } |
| depth++; |
| } |
| |
| 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); |
| } |
| } |
| } |