/* * 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.transformation; import org.apache.avalon.framework.configuration.Configuration; import org.apache.avalon.framework.configuration.ConfigurationException; import org.apache.avalon.framework.parameters.Parameters; import org.apache.avalon.framework.service.ServiceSelector; import org.apache.avalon.framework.thread.ThreadSafe; import org.apache.cocoon.ProcessingException; import org.apache.cocoon.acting.ValidatorActionResult; import org.apache.cocoon.transformation.helpers.FormValidatorHelper; import org.apache.cocoon.components.modules.input.InputModule; import org.apache.cocoon.environment.SourceResolver; import org.apache.cocoon.util.HashMap; import org.apache.cocoon.xml.dom.DOMStreamer; import org.apache.commons.lang.BooleanUtils; import org.w3c.dom.DocumentFragment; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; import java.io.IOException; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; /** * @cocoon.sitemap.component.documentation * Eliminates the need for XSP to use FormValidatorAction or HTML forms. * Caveat: Select options need a value attribute to work correctly. * * @cocoon.sitemap.component.name simple-form * @cocoon.sitemap.component.logger sitemap.transformer.simple-form * * * <p>This transformer fills all HTML 4 form elements with values from * an InputModule, e.g. request, with the same name. It handles select * boxes, textareas, checkboxes, radio buttons, password and text * fields, and buttons. Form elements and complete forms can be protected * from substitution by adding an attribute fixed="true" to them.</p> * * <p>In addition, it handles FormValidatorAction results by * selectively omitting <error/> elements. These elements need a * "name" attribute corresponding to the name of the form element, and * either a "when" or a "when-ge" attribute.</p> * * <p>An error element is send down the pipeline if validation results * are available and either the results equals the "when" attribute or * the result is greater or equal to the "when-ge" attribute.</p> * * <p>Names for validation results are "ok", "not-present", "error", * "is-null", "too-small", "too-large", and "no-match" for the similar * named values from ValidatorActionResult.</p> * * <p>There need not to be an "error" element for every form element, * multiple error elements for the same form element may be * present.</p> * * <p><em>Names of error elements are never augmented by prefix, suffix or * form name.</em></p> * * <p>Page parts with multiple occurrences depending on the number of * actual parameters can be enclosed in <repeat on="expr" using="var"/> * elements. <em>expr</em> is used to determine the number of occurrences * and <em>var</em> will be expanded with the ordinary number. Repeat elements * can be nested.</p> * * <p>Example:</p> * <pre> * <repeat on="mult" using="i"><input type="text" name="mult[${i}]"/></repeat> * </pre> * <p>Will include as many input elements as mult parameters are present. Adding * the repeater variable to the elements name is necessary only with structured * parameters or when they should be numbered. See also the <em>strip-number</em> * configuration parameter.</p> * * <p>To use this transformer, add the following to your * transformation pipeline: <pre> * <map:transform type="simple-form"/> * </pre></p> * * <p>Configuration elements: * <table> * <tr><td>input-module</td><td>(String) InputModule configuration, * defaults to an empty configuration and the "request-param" module</td></tr> * <tr><td>fixed-attribute</td><td>(String) Name of the attribute used to * indicate that this element should not be changed. ("fixed")</td></tr> * <tr><td>use-form-name</td><td>(boolean) Add the name of the form to the * name of form elements. Uses default Separator , if default separator is null * or empty, separator is set to "/". ("false")</td></tr> * <tr><td>use-form-name-twice</td><td>(boolean) Add the name of the form twice to the * name of form elements. This is useful when the form instance has no * all enclosing root tag and the form name is used instead <em>and</em> the * form name needs to be used to find the form data. Uses default Separator , * if default separator is null or empty, separator is set to "/".("false")</td></tr> * <tr><td>separator</td><td>(String) Separator between form name and element name ("/") * </td></tr> * <tr><td>prefix</td><td>(String) Prefix to add to element name for value lookup. No * separator will be added between prefix and rest of the name. Default * is "", when use-form-name is set, defaults to separator.</td></tr> * <tr><td>suffix</td><td>(String) Added to the input element's name. No * separator will be added between rest of the name and suffix. ("")</td></tr> * <tr><td>ignore-validation</td><td>(boolean) If set to true, all error * tags are copied as is regardless of the validation results.("false")</td></tr> * <tr><td>decoration</td><td>(int) Length of decorations around repeat variable. Example: * when using JXPath based module, decoration would be "[" and "]", hence 1. (1)</td></tr> * <tr><td>strip-number</td><td>(boolean) If set to false, element names of repeated * elements will contain the expanded repeater variable. ("true")</td></tr> * </table> * </p> * * <p>Sitemap parameters: * <table> * <tr><td>fixed</td><td>(boolean) Do not change values</td></tr> * <tr><td>prefix</td><td>(String) Added to the input element's name</td></tr> * <tr><td>suffix</td><td>(String) Added to the input element's name</td></tr> * <tr><td>input</td><td>(String) InputModule name</td></tr> * <tr><td>decoration</td><td>(int) Length of decorations around repeat variable.</td></tr> * <tr><td>strip-number</td><td>(boolean) Expanded repeater variable.</td></tr> * </table> * </p> * * <p>Example:<pre> * <input name="user.name" size="50" maxlength="60"/> * <error name="user.name" when-ge="error">required</error> * </pre></p> * * @author <a href="mailto:haul@apache.org">Christian Haul</a> * @version $Id$ */ public class SimpleFormTransformer extends AbstractSAXTransformer { /** strip numbers from repeated element name attributes */ private boolean stripNumber = true; /** Symbolic names for elements */ /** unknown element */ private static final int ELEMENT_DEFAULT = 0; /** input element */ private static final int ELEMENT_INPUT = 1; /** select element */ private static final int ELEMENT_SELECT = 2; /** option element */ private static final int ELEMENT_OPTION = 3; /** textarea element */ private static final int ELEMENT_TXTAREA = 4; /** error element */ private static final int ELEMENT_ERROR = 5; /** form element */ private static final int ELEMENT_FORM = 6; /** repeat element */ private static final int ELEMENT_REPEAT = 7; /** default element as Integer (needed as default in org.apache.cocoon.util.HashMap.get()) */ private static final Integer defaultElement = new Integer(ELEMENT_DEFAULT); /** input type unknown */ private static final int TYPE_DEFAULT = 0; /** input type checkbox */ private static final int TYPE_CHECKBOX = 1; /** input type radio */ private static final int TYPE_RADIO = 2; /** default input type as Integer (needed as default in org.apache.cocoon.util.HashMap.get()) */ private static final Integer defaultType = new Integer(TYPE_DEFAULT); protected static final String INPUT_MODULE_ROLE = InputModule.ROLE; protected static final String INPUT_MODULE_SELECTOR = INPUT_MODULE_ROLE + "Selector"; /** map element name string to symbolic name */ private static final HashMap elementNames; /** map input type string to symbolic name */ private static final HashMap inputTypes; /** map ValidatorActionResult to name string */ private static final HashMap validatorResults; /** map name string to ValidatorActionResult */ private static final HashMap validatorResultLabel; /** setup mapping tables */ static { HashMap names = new HashMap(); names.put("input", new Integer(ELEMENT_INPUT)); names.put("select", new Integer(ELEMENT_SELECT)); names.put("option", new Integer(ELEMENT_OPTION)); names.put("textarea", new Integer(ELEMENT_TXTAREA)); names.put("error", new Integer(ELEMENT_ERROR)); names.put("form", new Integer(ELEMENT_FORM)); names.put("repeat", new Integer(ELEMENT_REPEAT)); elementNames = names; names = null; names = new HashMap(); names.put("checkbox", new Integer(TYPE_CHECKBOX)); names.put("radio", new Integer(TYPE_RADIO)); inputTypes = names; names = null; names = new HashMap(); names.put("ok", ValidatorActionResult.OK); names.put("not-present", ValidatorActionResult.NOTPRESENT); names.put("error", ValidatorActionResult.ERROR); names.put("is-null", ValidatorActionResult.ISNULL); names.put("too-small", ValidatorActionResult.TOOSMALL); names.put("too-large", ValidatorActionResult.TOOLARGE); names.put("no-match", ValidatorActionResult.NOMATCH); validatorResultLabel = names; names = new HashMap(); names.put(ValidatorActionResult.OK, "ok"); names.put(ValidatorActionResult.NOTPRESENT, "not-present"); names.put(ValidatorActionResult.ERROR, "error"); names.put(ValidatorActionResult.ISNULL, "is-null"); names.put(ValidatorActionResult.TOOSMALL, "too-small"); names.put(ValidatorActionResult.TOOLARGE, "too-large"); names.put(ValidatorActionResult.NOMATCH, "no-match"); validatorResults = names; names = null; } /** current element's request parameter values */ protected Object[] values; /** current request's validation results (all validated elements) */ protected Map validationResults; /** Should we skip inserting values? */ private boolean fixed; /** Is the complete document protected? */ private boolean documentFixed; private String fixedName = "fixed"; private String prefix; private String suffix; private String defaultPrefix; private String defaultSuffix; private String separator; private String formName; private boolean useFormName; private boolean useFormNameTwice; private boolean ignoreValidation; private int decorationSize = 1; private String defaultInput = "request-param"; private Configuration defaultInputConf; private Configuration inputConf; private InputModule input; private ServiceSelector inputSelector; private String inputName; /** Skip element's content only. Otherwise skip also surrounding element. */ protected boolean skipChildrenOnly; /** Count nested repeat elements. */ protected int recordingCount; /** List of {@link RepeaterStatus} elements keeping track of nested repeat blocks. */ protected List repeater; /** Map of {@link ValueList} to track multiple parameters. */ protected Map formValues; /** * Keep track of repeater status. */ protected static class RepeaterStatus { public String var = null; public String expr = null; public int count = 0; public RepeaterStatus(String var, int count, String expr) { this.var = var; this.count = count; this.expr = expr; } public String toString() { return "[" + this.var + "," + this.expr + "," + this.count + "]"; } } /** * Keep track of multiple values. */ protected static class ValueList { private int current = -1; private Object[] values = null; public ValueList(Object[] values) { this.values = values; this.current = (values != null && values.length > 0 ? 0 : -1); } public Object getNext() { Object result = null; if (this.values != null) { if (this.current < this.values.length) { result = this.values[this.current++]; } } return result; } } public SimpleFormTransformer() { this.defaultNamespaceURI = ""; } /** set per instance variables to defaults */ private void reset() { this.skipChildrenOnly = false; this.values = null; this.validationResults = null; this.documentFixed = false; this.fixed = false; this.formName = null; this.recordingCount = 0; this.repeater = new LinkedList(); this.formValues = new HashMap(); if (this.inputSelector != null) { if (this.input != null) this.inputSelector.release(this.input); this.manager.release(this.inputSelector); } } /** * Avalon Configurable Interface */ public void configure(Configuration config) throws ConfigurationException { super.configure(config); this.defaultInputConf = config.getChild("input-module"); this.defaultInput = this.defaultInputConf.getAttribute("name", this.defaultInput); this.separator = config.getChild("separator").getValue(this.separator); this.defaultPrefix = config.getChild("prefix").getValue(this.defaultPrefix); this.defaultSuffix = config.getChild("suffix").getValue(this.defaultSuffix); this.fixedName = config.getChild("fixed-attribute").getValue(this.fixedName); this.useFormName = config.getChild("use-form-name").getValueAsBoolean(this.useFormName); this.useFormNameTwice = config.getChild("use-form-name-twice").getValueAsBoolean(this.useFormNameTwice); this.useFormName = this.useFormName || this.useFormNameTwice; if (this.useFormName) { this.separator = (this.separator == null || this.separator.length() == 0 ? "/" : this.separator); this.defaultPrefix = this.separator; } this.ignoreValidation = config.getChild("ignore-validation").getValueAsBoolean(this.ignoreValidation); this.decorationSize = config.getChild("decoration").getValueAsInteger(this.decorationSize); this.stripNumber = config.getChild("strip-number").getValueAsBoolean(this.stripNumber); } /** * Read sitemap parameters and set properties accordingly. */ private void evaluateParameters() { this.documentFixed = this.parameters.getParameterAsBoolean("fixed", false); this.fixed = this.documentFixed; this.prefix = this.parameters.getParameter("prefix", this.defaultPrefix); this.suffix = this.parameters.getParameter("suffix", this.defaultSuffix); this.inputName = this.parameters.getParameter("input", null); this.decorationSize = this.parameters.getParameterAsInteger("decoration", this.decorationSize); this.stripNumber = this.parameters.getParameterAsBoolean("strip-number", this.stripNumber); } /** * Setup the next round. * The instance variables are initialised. * @param resolver The current SourceResolver * @param objectModel The objectModel of the environment. * @param src The value of the src attribute in the sitemap. * @param par The parameters from the sitemap. */ public void setup(SourceResolver resolver, Map objectModel, String src, Parameters par) throws ProcessingException, SAXException, IOException { this.reset(); super.setup(resolver, objectModel, src, par); if (request == null) { getLogger().debug("no request object"); throw new ProcessingException("no request object"); } this.evaluateParameters(); this.setupInputModule(); } /** * Setup and obtain reference to the input module. */ private void setupInputModule() { this.inputConf = null; if (this.ignoreValidation) { this.validationResults = null; } else { this.validationResults = FormValidatorHelper.getResults(this.objectModel); } if (this.inputName == null) { this.inputName = this.defaultInput; this.inputConf = this.defaultInputConf; } try { // obtain input module this.inputSelector = (ServiceSelector) this.manager.lookup(INPUT_MODULE_SELECTOR); if (this.inputName != null && this.inputSelector != null && this.inputSelector.isSelectable(this.inputName)) { this.input = (InputModule) this.inputSelector.select(this.inputName); if (!(this.input instanceof ThreadSafe && this.inputSelector instanceof ThreadSafe)) { this.inputSelector.release(this.input); this.manager.release(this.inputSelector); this.input = null; this.inputSelector = null; } } else { if (this.inputName != null) if (getLogger().isErrorEnabled()) getLogger().error( "A problem occurred setting up '" + this.inputName + "': Selector is " + (this.inputSelector != null ? "not " : "") + "null, Component is " + (this.inputSelector != null && this.inputSelector.isSelectable(this.inputName) ? "known" : "unknown")); } } catch (Exception e) { if (getLogger().isWarnEnabled()) getLogger().warn( "A problem occurred setting up '" + this.inputName + "': " + e.getMessage()); } } /** * Recycle this component. */ public void recycle() { reset(); super.recycle(); } /** * Generate string representation of attributes. For debug only. */ protected String printAttributes(Attributes attr) { StringBuffer sb = new StringBuffer(); sb.append('['); for (int i = 0; i < attr.getLength(); i++) { sb.append('@').append(attr.getLocalName(i)).append("='").append( attr.getValue(i)).append( "' "); } sb.append(']'); return sb.toString(); } /** * Handle input elements that may have a "checked" attributes, * i.e. checkbox and radio. */ protected void startCheckableElement( String aName, String uri, String name, String raw, AttributesImpl attributes) throws SAXException { // @fixed and this.fixed already considered in startInputElement this.values = this.getValues(aName); String checked = attributes.getValue("checked"); String value = attributes.getValue("value"); boolean found = false; if (getLogger().isDebugEnabled()) getLogger().debug( "startCheckableElement " + name + " attributes " + this.printAttributes(attributes)); if (this.values != null) { if (getLogger().isDebugEnabled()) getLogger().debug("replacing"); for (int i = 0; i < this.values.length; i++) { if (this.values[i].equals(value)) { found = true; if (checked == null) { attributes.addAttribute("", "checked", "checked", "CDATA", ""); } break; } } if (!found && checked != null) { attributes.removeAttribute(attributes.getIndex("checked")); } } this.relayStartElement(uri, name, raw, attributes); } /** * Handle input elements that may don't have a "checked" * attributes, e.g. text, password, button. */ protected void startNonCheckableElement( String aName, String uri, String name, String raw, AttributesImpl attributes) throws SAXException { // @fixed and this.fixed already considered in startInputElement Object fValue = this.getNextValue(aName); String value = attributes.getValue("value"); if (getLogger().isDebugEnabled()) getLogger().debug( "startNonCheckableElement " + name + " attributes " + this.printAttributes(attributes)); if (fValue != null) { if (getLogger().isDebugEnabled()) getLogger().debug("replacing"); if (value != null) { attributes.setValue(attributes.getIndex("value"), String.valueOf(fValue)); } else { attributes.addAttribute("", "value", "value", "CDATA", String.valueOf(fValue)); } } this.relayStartElement(uri, name, raw, attributes); } /** * Handle input elements. Calls startCheckableElement or * startNonCheckableElement. */ protected void startInputElement(String uri, String name, String raw, Attributes attr) throws SAXException { // @value = request.getParameterValues(@name) String aName = getName(attr.getValue("name")); String fixed = attr.getValue(this.fixedName); if (getLogger().isDebugEnabled()) getLogger().debug( "startInputElement " + name + " attributes " + this.printAttributes(attr)); if (aName == null || this.fixed || (fixed != null && BooleanUtils.toBoolean(fixed))) { this.relayStartElement(uri, name, raw, attr); } else { if (getLogger().isDebugEnabled()) getLogger().debug("replacing"); attr = this.normalizeAttributes(attr); AttributesImpl attributes = null; if (attr instanceof AttributesImpl) { attributes = (AttributesImpl) attr; } else { attributes = new AttributesImpl(attr); } String type = attributes.getValue("type"); switch (((Integer) inputTypes.get(type, defaultType)).intValue()) { case TYPE_CHECKBOX : case TYPE_RADIO : this.startCheckableElement(aName, uri, name, raw, attributes); break; case TYPE_DEFAULT : this.startNonCheckableElement(aName, uri, name, raw, attributes); break; } this.values = null; } } /** * Handle select elements. Sets up some instance variables for * following option elements. */ protected void startSelectElement(String uri, String name, String raw, Attributes attr) throws SAXException { // this.values = request.getParameterValues(@name) String aName = getName(attr.getValue("name")); String fixed = attr.getValue(this.fixedName); this.values = null; if (getLogger().isDebugEnabled()) getLogger().debug( "startSelectElement " + name + " attributes " + this.printAttributes(attr)); if (aName != null && !(this.fixed || (fixed != null && BooleanUtils.toBoolean(fixed)))) { if (attr.getIndex("multiple") > -1) { this.values = this.getValues(aName); } else { Object val = this.getNextValue(aName); if (val != null) { this.values = new Object[1]; this.values[0] = val; } else { this.values = null; } } attr = this.normalizeAttributes(attr); } this.relayStartElement(uri, name, raw, attr); } /** * Handle option elements. Uses instance variables set up by * startSelectElement. Relies on option having a "value" * attribute, i.e. does not check following characters if "value" * is not present. */ protected void startOptionElement(String uri, String name, String raw, Attributes attr) throws SAXException { // add @selected if @value in request.getParameterValues(@name) if (getLogger().isDebugEnabled()) getLogger().debug( "startOptionElement " + name + " attributes " + this.printAttributes(attr)); if (this.values == null || this.fixed) { this.relayStartElement(uri, name, raw, attr); } else { if (getLogger().isDebugEnabled()) getLogger().debug("replacing"); AttributesImpl attributes = null; if (attr instanceof AttributesImpl) { attributes = (AttributesImpl) attr; } else { attributes = new AttributesImpl(attr); } String selected = attributes.getValue("selected"); String value = attributes.getValue("value"); boolean found = false; for (int i = 0; i < this.values.length; i++) { if (this.values[i].equals(value)) { found = true; if (selected == null) { attributes.addAttribute("", "selected", "selected", "CDATA", ""); } break; } } if (!found && selected != null) { attributes.removeAttribute(attributes.getIndex("selected")); } this.relayStartElement(uri, name, raw, attributes); } } /** * Handles textarea elements. Skips nested events if request * parameter with same name exists. */ protected void startTextareaElement(String uri, String name, String raw, Attributes attributes) throws SAXException { String aName = getName(attributes.getValue("name")); String fixed = attributes.getValue(this.fixedName); Object value = null; if (getLogger().isDebugEnabled()) getLogger().debug( "startTextareaElement " + name + " attributes " + this.printAttributes(attributes)); if (aName != null) { value = this.getNextValue(aName); } if (value == null || this.fixed || (fixed != null && BooleanUtils.toBoolean(fixed))) { this.relayStartElement(uri, name, raw, attributes); } else { if (getLogger().isDebugEnabled()) getLogger().debug("replacing"); this.relayStartElement(uri, name, raw, this.normalizeAttributes(attributes)); String valString = String.valueOf(value); this.characters(valString.toCharArray(), 0, valString.length()); // well, this doesn't really work out nicely. do it the hard way. if (this.ignoreEventsCount == 0) this.skipChildrenOnly = true; this.ignoreEventsCount++; } } /** * Handle error elements. If validation results are available, * compares validation result for parameter with the same name as * the "name" attribute with the result names is "when" and * "when-ge". Drops element and all nested events when error * condition is not met. */ protected void startErrorElement(String uri, String name, String raw, Attributes attr) throws SAXException { if (getLogger().isDebugEnabled()) getLogger().debug( "startErrorElement " + name + " attributes " + this.printAttributes(attr)); if (this.ignoreValidation) { this.relayStartElement(uri, name, raw, attr); } else if (this.validationResults == null || this.fixed) { this.relayStartElement(true, false, uri, name, raw, attr); } else { String aName = attr.getValue("name"); if (aName == null) { this.relayStartElement(uri, name, raw, attr); } else { ValidatorActionResult validation = FormValidatorHelper.getParamResult(this.objectModel, aName); String when = attr.getValue("when"); String when_ge = attr.getValue("when-ge"); if ((when != null && when.equals(validatorResults.get(validation))) || (when_ge != null && validation.ge( (ValidatorActionResult) validatorResultLabel.get( when_ge, ValidatorActionResult.MAXERROR)))) { AttributesImpl attributes = null; if (attr instanceof AttributesImpl) { attributes = (AttributesImpl) attr; } else { attributes = new AttributesImpl(attr); } // remove attributes not meant for client attributes.removeAttribute(attributes.getIndex("name")); if (when != null) attributes.removeAttribute(attributes.getIndex("when")); if (when_ge != null) attributes.removeAttribute(attributes.getIndex("when-ge")); this.relayStartElement(uri, name, raw, this.normalizeAttributes(attributes)); } else { this.relayStartElement(true, true, uri, name, raw, attr); } } } } /** * Start processing a form element. Sets protection indicator if attribute * "fixed" is present and either "true" or "yes". Removes attribute "fixed" * if present. * @param uri The namespace of the element. * @param name The local name of the element. * @param raw The qualified name of the element. * @param attr The attributes of the element. */ protected void startFormElement(String uri, String name, String raw, Attributes attr) throws SAXException { String fixed = attr.getValue(this.fixedName); if (this.useFormName) { this.formName = attr.getValue("name"); } if (fixed == null) { this.relayStartElement(uri, name, raw, attr); } else { if (!this.fixed && BooleanUtils.toBoolean(fixed)) { this.fixed = true; } // remove attributes not meant for client AttributesImpl attributes = null; if (attr instanceof AttributesImpl) { attributes = (AttributesImpl) attr; } else { attributes = new AttributesImpl(attr); } attributes.removeAttribute(attributes.getIndex(this.fixedName)); this.relayStartElement(uri, name, raw, this.normalizeAttributes(attributes)); } } /** * Start recording repeat element contents and push repeat expression and * variable to repeater stack. Only start recording, if no other recorder is * currently running. * * @param uri * @param name * @param raw * @param attr * @throws SAXException */ protected void startRepeatElement(String uri, String name, String raw, Attributes attr) throws SAXException { if (this.recordingCount == 0) { if (!(this.fixed || BooleanUtils.toBoolean(attr.getValue(this.fixedName)))) { RepeaterStatus status = new RepeaterStatus("${" + attr.getValue("using") + "}", 0, attr.getValue("on")); this.repeater.add(status); this.startRecording(); this.recordingCount++; } else { this.relayStartElement(uri, name, raw, attr); } } else { this.relayStartElement(uri, name, raw, attr); this.recordingCount++; } } /** * Stop recording repeat contents and replay required number of times. * Stop only if outmost repeat element is ending. * * @param uri * @param name * @param raw * @throws SAXException */ protected void endRepeatElement(String uri, String name, String raw) throws SAXException { this.recordingCount--; if (this.recordingCount == 0) { DocumentFragment fragment = this.endRecording(); RepeaterStatus status = (RepeaterStatus) this.repeater.get(this.repeater.size() - 1); Object[] vals = this.getValues(this.getName(status.expr)); int count = (vals != null ? vals.length : 0); for (status.count = 1; status.count <= count; status.count++) { DOMStreamer streamer = new DOMStreamer(this, this); streamer.stream(fragment); } this.repeater.remove(this.repeater.size() - 1); } else { this.relayEndElement(uri, name, raw); if (this.recordingCount < 0) { this.recordingCount = 0; } } } /** * Start processing elements of our namespace. * This hook is invoked for each sax event with our namespace. * @param uri The namespace of the element. * @param name The local name of the element. * @param raw The qualified name of the element. * @param attr The attributes of the element. */ public void startTransformingElement(String uri, String name, String raw, Attributes attr) throws SAXException { if (this.ignoreEventsCount == 0 && this.recordingCount == 0) { switch (((Integer) elementNames.get(name, defaultElement)).intValue()) { case ELEMENT_INPUT : this.startInputElement(uri, name, raw, attr); break; case ELEMENT_SELECT : this.startSelectElement(uri, name, raw, attr); break; case ELEMENT_OPTION : this.startOptionElement(uri, name, raw, attr); break; case ELEMENT_TXTAREA : this.startTextareaElement(uri, name, raw, attr); break; case ELEMENT_ERROR : this.startErrorElement(uri, name, raw, attr); break; case ELEMENT_FORM : this.startFormElement(uri, name, raw, attr); break; case ELEMENT_REPEAT : this.startRepeatElement(uri, name, raw, attr); break; default : this.relayStartElement(uri, name, raw, attr); } } else if (this.recordingCount > 0) { switch (((Integer) elementNames.get(name, defaultElement)).intValue()) { case ELEMENT_REPEAT : this.startRepeatElement(uri, name, raw, attr); break; default : this.relayStartElement(uri, name, raw, attr); } } else { this.relayStartElement(uri, name, raw, attr); } } /** * Start processing elements of our namespace. * This hook is invoked for each sax event with our namespace. * @param uri The namespace of the element. * @param name The local name of the element. * @param raw The qualified name of the element. */ public void endTransformingElement(String uri, String name, String raw) throws SAXException { if (this.ignoreEventsCount > 0) { this.relayEndElement(uri, name, raw); } else if (this.recordingCount > 0) { switch (((Integer) elementNames.get(name, defaultElement)).intValue()) { case ELEMENT_REPEAT : this.endRepeatElement(uri, name, raw); break; default : this.relayEndElement(uri, name, raw); } } else { switch (((Integer) elementNames.get(name, defaultElement)).intValue()) { case ELEMENT_SELECT : this.values = null; this.relayEndElement(uri, name, raw); break; case ELEMENT_INPUT : case ELEMENT_OPTION : case ELEMENT_TXTAREA : case ELEMENT_ERROR : this.relayEndElement(uri, name, raw); break; case ELEMENT_FORM : this.fixed = this.documentFixed; this.formName = null; this.relayEndElement(uri, name, raw); break; case ELEMENT_REPEAT : this.endRepeatElement(uri, name, raw); break; default : this.relayEndElement(uri, name, raw); } } } /** * Remove extra information from element's attributes. Currently only removes * the repeater variable from the element's name attribute if present. * * @param attr * @return modified attributes */ private Attributes normalizeAttributes(Attributes attr) { Attributes result = attr; if (this.stripNumber && this.repeater.size() > 0) { String name = attr.getValue("name"); if (name != null) { for (Iterator i = this.repeater.iterator(); i.hasNext();) { RepeaterStatus status = (RepeaterStatus) i.next(); int pos = name.indexOf(status.var); if (pos >= 0) { AttributesImpl attributes; if (result instanceof AttributesImpl) { attributes = (AttributesImpl) result; } else { attributes = new AttributesImpl(result); } name = name.substring(0, pos - this.decorationSize) + name.substring(pos + status.var.length() + this.decorationSize); attributes.setValue(attributes.getIndex("name"), name); result = attributes; } } } } return result; } /** * Generate the "real" name of an element for value lookup. * @param name * @return "real" name. */ private String getName(String name) { String result = name; if (this.useFormName && this.formName != null) { if (this.separator != null) { if (this.useFormNameTwice) { result = this.formName + this.separator + this.formName + this.separator + result; } else { result = this.formName + this.separator + result; } } else { if (this.useFormNameTwice) { result = this.formName + result; } else { // does this make sense ? result = this.formName + this.formName + result; } } } if (this.prefix != null) { result = this.prefix + result; } if (this.suffix != null) { result = result + this.prefix; } if (this.repeater.size() > 0) { for (Iterator i = this.repeater.iterator(); i.hasNext();) { RepeaterStatus status = (RepeaterStatus) i.next(); int pos = result.indexOf(status.var); if (pos != -1) { result = result.substring(0, pos) + status.count + result.substring(pos + status.var.length()); } } } return result; } /** * Obtain values from used InputModule if not done already and return the * next value. If no more values exist, returns null. * * @param name */ private Object getNextValue(String name) { Object result = null; if (this.formValues.containsKey(name)) { ValueList vList = (ValueList) this.formValues.get(name); result = vList.getNext(); } else { ValueList vList = new ValueList(this.getValues(name)); result = vList.getNext(); this.formValues.put(name, vList); } return result; } /** * Obtain values from the used InputModule. */ private Object[] getValues(String name) { Object[] values = null; ServiceSelector iputSelector = null; InputModule iput = null; try { if (this.input != null) { // input module is thread safe // thus we still have a reference to it values = input.getAttributeValues(name, this.inputConf, objectModel); if (getLogger().isDebugEnabled()) getLogger().debug("cached module " + this.input + " attribute " + name + " returns " + values); } else { // input was not thread safe // so acquire it again iputSelector = (ServiceSelector)this.manager.lookup(INPUT_MODULE_SELECTOR); if (this.inputName != null && iputSelector != null && iputSelector.isSelectable(this.inputName)) { iput = (InputModule) iputSelector.select(this.inputName); } if (iput != null) { values = iput.getAttributeValues(name, this.inputConf, objectModel); } if (getLogger().isDebugEnabled()) getLogger().debug( "fresh module " + iput + " attribute " + name + " returns " + values); } } catch (Exception e) { if (getLogger().isWarnEnabled()) getLogger().warn( "A problem occurred acquiring a value from '" + this.inputName + "' for '" + name + "': " + e.getMessage()); } finally { // release components if necessary if (iputSelector != null) { if (iput != null) iputSelector.release(iput); this.manager.release(iputSelector); } } return values; } /** * Calls the super's method startTransformingElement. * * @param uri * @param name * @param raw * @param attr * @throws SAXException */ protected void relayStartElement(String uri, String name, String raw, Attributes attr) throws SAXException { this.relayStartElement(false, false, uri, name, raw, attr); } /** * Calls the super's method startTransformingElement and increments the * ignoreEventsCount if skip is true. Increment can be done either before * invoking super's method, so that the element itself is skipped, or afterwards, * so that only the children are skipped. * * @param skip * @param skipChildrenOnly * @param uri * @param name * @param raw * @param attr * @throws SAXException */ protected void relayStartElement( boolean skip, boolean skipChildrenOnly, String uri, String name, String raw, Attributes attr) throws SAXException { try { if (this.ignoreEventsCount > 0) { this.ignoreEventsCount++; super.startTransformingElement(uri, name, raw, attr); } else { if (skip) this.skipChildrenOnly = skipChildrenOnly; if (skip && !skipChildrenOnly) this.ignoreEventsCount++; super.startTransformingElement(uri, name, raw, attr); if (skip && skipChildrenOnly) this.ignoreEventsCount++; } } catch (ProcessingException e) { throw new SAXException(e); } catch (IOException e) { throw new SAXException(e); } } /** * Calls the super's method endTransformingElement and decrements the * ignoreEventsCount if larger than zero. * * @param uri * @param name * @param raw * @throws SAXException */ protected void relayEndElement(String uri, String name, String raw) throws SAXException { if (this.ignoreEventsCount == 1 && this.skipChildrenOnly) this.ignoreEventsCount--; try { super.endTransformingElement(uri, name, raw); } catch (ProcessingException e) { throw new SAXException(e); } catch (IOException e) { throw new SAXException(e); } catch (Exception e) { getLogger().error("exception", e); } if (this.ignoreEventsCount > 0) this.ignoreEventsCount--; } }