1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.cocoon.transformation;
18
19 import org.apache.avalon.framework.configuration.Configuration;
20 import org.apache.avalon.framework.configuration.ConfigurationException;
21 import org.apache.avalon.framework.parameters.Parameters;
22 import org.apache.avalon.framework.service.ServiceSelector;
23 import org.apache.avalon.framework.thread.ThreadSafe;
24
25 import org.apache.cocoon.ProcessingException;
26 import org.apache.cocoon.acting.ValidatorActionResult;
27 import org.apache.cocoon.transformation.helpers.FormValidatorHelper;
28 import org.apache.cocoon.components.modules.input.InputModule;
29 import org.apache.cocoon.environment.SourceResolver;
30 import org.apache.cocoon.util.HashMap;
31 import org.apache.cocoon.xml.dom.DOMStreamer;
32 import org.apache.commons.lang.BooleanUtils;
33
34 import org.w3c.dom.DocumentFragment;
35 import org.xml.sax.Attributes;
36 import org.xml.sax.SAXException;
37 import org.xml.sax.helpers.AttributesImpl;
38
39 import java.io.IOException;
40 import java.util.Iterator;
41 import java.util.LinkedList;
42 import java.util.List;
43 import java.util.Map;
44
45 /**
46 * @cocoon.sitemap.component.documentation
47 * Eliminates the need for XSP to use FormValidatorAction or HTML forms.
48 * Caveat: Select options need a value attribute to work correctly.
49 *
50 * @cocoon.sitemap.component.name simple-form
51 * @cocoon.sitemap.component.logger sitemap.transformer.simple-form
52 *
53 *
54 * <p>This transformer fills all HTML 4 form elements with values from
55 * an InputModule, e.g. request, with the same name. It handles select
56 * boxes, textareas, checkboxes, radio buttons, password and text
57 * fields, and buttons. Form elements and complete forms can be protected
58 * from substitution by adding an attribute fixed="true" to them.</p>
59 *
60 * <p>In addition, it handles FormValidatorAction results by
61 * selectively omitting <error/> elements. These elements need a
62 * "name" attribute corresponding to the name of the form element, and
63 * either a "when" or a "when-ge" attribute.</p>
64 *
65 * <p>An error element is send down the pipeline if validation results
66 * are available and either the results equals the "when" attribute or
67 * the result is greater or equal to the "when-ge" attribute.</p>
68 *
69 * <p>Names for validation results are "ok", "not-present", "error",
70 * "is-null", "too-small", "too-large", and "no-match" for the similar
71 * named values from ValidatorActionResult.</p>
72 *
73 * <p>There need not to be an "error" element for every form element,
74 * multiple error elements for the same form element may be
75 * present.</p>
76 *
77 * <p><em>Names of error elements are never augmented by prefix, suffix or
78 * form name.</em></p>
79 *
80 * <p>Page parts with multiple occurrences depending on the number of
81 * actual parameters can be enclosed in <repeat on="expr" using="var"/>
82 * elements. <em>expr</em> is used to determine the number of occurrences
83 * and <em>var</em> will be expanded with the ordinary number. Repeat elements
84 * can be nested.</p>
85 *
86 * <p>Example:</p>
87 * <pre>
88 * <repeat on="mult" using="i"><input type="text" name="mult[${i}]"/></repeat>
89 * </pre>
90 * <p>Will include as many input elements as mult parameters are present. Adding
91 * the repeater variable to the elements name is necessary only with structured
92 * parameters or when they should be numbered. See also the <em>strip-number</em>
93 * configuration parameter.</p>
94 *
95 * <p>To use this transformer, add the following to your
96 * transformation pipeline: <pre>
97 * <map:transform type="simple-form"/>
98 * </pre></p>
99 *
100 * <p>Configuration elements:
101 * <table>
102 * <tr><td>input-module</td><td>(String) InputModule configuration,
103 * defaults to an empty configuration and the "request-param" module</td></tr>
104 * <tr><td>fixed-attribute</td><td>(String) Name of the attribute used to
105 * indicate that this element should not be changed. ("fixed")</td></tr>
106 * <tr><td>use-form-name</td><td>(boolean) Add the name of the form to the
107 * name of form elements. Uses default Separator , if default separator is null
108 * or empty, separator is set to "/". ("false")</td></tr>
109 * <tr><td>use-form-name-twice</td><td>(boolean) Add the name of the form twice to the
110 * name of form elements. This is useful when the form instance has no
111 * all enclosing root tag and the form name is used instead <em>and</em> the
112 * form name needs to be used to find the form data. Uses default Separator ,
113 * if default separator is null or empty, separator is set to "/".("false")</td></tr>
114 * <tr><td>separator</td><td>(String) Separator between form name and element name ("/")
115 * </td></tr>
116 * <tr><td>prefix</td><td>(String) Prefix to add to element name for value lookup. No
117 * separator will be added between prefix and rest of the name. Default
118 * is "", when use-form-name is set, defaults to separator.</td></tr>
119 * <tr><td>suffix</td><td>(String) Added to the input element's name. No
120 * separator will be added between rest of the name and suffix. ("")</td></tr>
121 * <tr><td>ignore-validation</td><td>(boolean) If set to true, all error
122 * tags are copied as is regardless of the validation results.("false")</td></tr>
123 * <tr><td>decoration</td><td>(int) Length of decorations around repeat variable. Example:
124 * when using JXPath based module, decoration would be "[" and "]", hence 1. (1)</td></tr>
125 * <tr><td>strip-number</td><td>(boolean) If set to false, element names of repeated
126 * elements will contain the expanded repeater variable. ("true")</td></tr>
127 * </table>
128 * </p>
129 *
130 * <p>Sitemap parameters:
131 * <table>
132 * <tr><td>fixed</td><td>(boolean) Do not change values</td></tr>
133 * <tr><td>prefix</td><td>(String) Added to the input element's name</td></tr>
134 * <tr><td>suffix</td><td>(String) Added to the input element's name</td></tr>
135 * <tr><td>input</td><td>(String) InputModule name</td></tr>
136 * <tr><td>decoration</td><td>(int) Length of decorations around repeat variable.</td></tr>
137 * <tr><td>strip-number</td><td>(boolean) Expanded repeater variable.</td></tr>
138 * </table>
139 * </p>
140 *
141 * <p>Example:<pre>
142 * <input name="user.name" size="50" maxlength="60"/>
143 * <error name="user.name" when-ge="error">required</error>
144 * </pre></p>
145 *
146 * @author <a href="mailto:haul@apache.org">Christian Haul</a>
147 * @version $Id: SimpleFormTransformer.java 433543 2006-08-22 06:22:54Z crossley $
148 */
149 public class SimpleFormTransformer extends AbstractSAXTransformer {
150
151 /** strip numbers from repeated element name attributes */
152 private boolean stripNumber = true;
153
154 /** Symbolic names for elements */
155 /** unknown element */
156 private static final int ELEMENT_DEFAULT = 0;
157 /** input element */
158 private static final int ELEMENT_INPUT = 1;
159 /** select element */
160 private static final int ELEMENT_SELECT = 2;
161 /** option element */
162 private static final int ELEMENT_OPTION = 3;
163 /** textarea element */
164 private static final int ELEMENT_TXTAREA = 4;
165 /** error element */
166 private static final int ELEMENT_ERROR = 5;
167 /** form element */
168 private static final int ELEMENT_FORM = 6;
169 /** repeat element */
170 private static final int ELEMENT_REPEAT = 7;
171 /** default element as Integer (needed as default in org.apache.cocoon.util.HashMap.get()) */
172 private static final Integer defaultElement = new Integer(ELEMENT_DEFAULT);
173
174 /** input type unknown */
175 private static final int TYPE_DEFAULT = 0;
176 /** input type checkbox */
177 private static final int TYPE_CHECKBOX = 1;
178 /** input type radio */
179 private static final int TYPE_RADIO = 2;
180 /** default input type as Integer (needed as default in org.apache.cocoon.util.HashMap.get()) */
181 private static final Integer defaultType = new Integer(TYPE_DEFAULT);
182
183 protected static final String INPUT_MODULE_ROLE = InputModule.ROLE;
184 protected static final String INPUT_MODULE_SELECTOR = INPUT_MODULE_ROLE + "Selector";
185
186 /** map element name string to symbolic name */
187 private static final HashMap elementNames;
188 /** map input type string to symbolic name */
189 private static final HashMap inputTypes;
190 /** map ValidatorActionResult to name string */
191 private static final HashMap validatorResults;
192 /** map name string to ValidatorActionResult */
193 private static final HashMap validatorResultLabel;
194
195 /** setup mapping tables */
196 static {
197 HashMap names = new HashMap();
198 names.put("input", new Integer(ELEMENT_INPUT));
199 names.put("select", new Integer(ELEMENT_SELECT));
200 names.put("option", new Integer(ELEMENT_OPTION));
201 names.put("textarea", new Integer(ELEMENT_TXTAREA));
202 names.put("error", new Integer(ELEMENT_ERROR));
203 names.put("form", new Integer(ELEMENT_FORM));
204 names.put("repeat", new Integer(ELEMENT_REPEAT));
205 elementNames = names;
206 names = null;
207
208 names = new HashMap();
209 names.put("checkbox", new Integer(TYPE_CHECKBOX));
210 names.put("radio", new Integer(TYPE_RADIO));
211 inputTypes = names;
212 names = null;
213
214 names = new HashMap();
215 names.put("ok", ValidatorActionResult.OK);
216 names.put("not-present", ValidatorActionResult.NOTPRESENT);
217 names.put("error", ValidatorActionResult.ERROR);
218 names.put("is-null", ValidatorActionResult.ISNULL);
219 names.put("too-small", ValidatorActionResult.TOOSMALL);
220 names.put("too-large", ValidatorActionResult.TOOLARGE);
221 names.put("no-match", ValidatorActionResult.NOMATCH);
222 validatorResultLabel = names;
223
224 names = new HashMap();
225 names.put(ValidatorActionResult.OK, "ok");
226 names.put(ValidatorActionResult.NOTPRESENT, "not-present");
227 names.put(ValidatorActionResult.ERROR, "error");
228 names.put(ValidatorActionResult.ISNULL, "is-null");
229 names.put(ValidatorActionResult.TOOSMALL, "too-small");
230 names.put(ValidatorActionResult.TOOLARGE, "too-large");
231 names.put(ValidatorActionResult.NOMATCH, "no-match");
232 validatorResults = names;
233 names = null;
234 }
235
236 /** current element's request parameter values */
237 protected Object[] values;
238
239 /** current request's validation results (all validated elements) */
240 protected Map validationResults;
241
242 /** Should we skip inserting values? */
243 private boolean fixed;
244 /** Is the complete document protected? */
245 private boolean documentFixed;
246
247 private String fixedName = "fixed";
248 private String prefix;
249 private String suffix;
250 private String defaultPrefix;
251 private String defaultSuffix;
252 private String separator;
253 private String formName;
254 private boolean useFormName;
255 private boolean useFormNameTwice;
256 private boolean ignoreValidation;
257 private int decorationSize = 1;
258
259 private String defaultInput = "request-param";
260 private Configuration defaultInputConf;
261 private Configuration inputConf;
262 private InputModule input;
263 private ServiceSelector inputSelector;
264 private String inputName;
265
266 /** Skip element's content only. Otherwise skip also surrounding element. */
267 protected boolean skipChildrenOnly;
268
269 /** Count nested repeat elements. */
270 protected int recordingCount;
271
272 /** List of {@link RepeaterStatus} elements keeping track of nested repeat blocks. */
273 protected List repeater;
274
275 /** Map of {@link ValueList} to track multiple parameters. */
276 protected Map formValues;
277
278 /**
279 * Keep track of repeater status.
280 */
281 protected static class RepeaterStatus {
282 public String var = null;
283 public String expr = null;
284 public int count = 0;
285
286 public RepeaterStatus(String var, int count, String expr) {
287 this.var = var;
288 this.count = count;
289 this.expr = expr;
290 }
291
292 public String toString() {
293 return "[" + this.var + "," + this.expr + "," + this.count + "]";
294 }
295 }
296
297 /**
298 * Keep track of multiple values.
299 */
300 protected static class ValueList {
301 private int current = -1;
302 private Object[] values = null;
303
304 public ValueList(Object[] values) {
305 this.values = values;
306 this.current = (values != null && values.length > 0 ? 0 : -1);
307 }
308
309 public Object getNext() {
310 Object result = null;
311 if (this.values != null) {
312 if (this.current < this.values.length) {
313 result = this.values[this.current++];
314 }
315 }
316 return result;
317 }
318 }
319
320 public SimpleFormTransformer() {
321 this.defaultNamespaceURI = "";
322 }
323
324 /** set per instance variables to defaults */
325 private void reset() {
326 this.skipChildrenOnly = false;
327 this.values = null;
328 this.validationResults = null;
329 this.documentFixed = false;
330 this.fixed = false;
331 this.formName = null;
332 this.recordingCount = 0;
333 this.repeater = new LinkedList();
334 this.formValues = new HashMap();
335
336 if (this.inputSelector != null) {
337 if (this.input != null)
338 this.inputSelector.release(this.input);
339 this.manager.release(this.inputSelector);
340 }
341 }
342
343 /**
344 * Avalon Configurable Interface
345 */
346 public void configure(Configuration config) throws ConfigurationException {
347 super.configure(config);
348
349 this.defaultInputConf = config.getChild("input-module");
350 this.defaultInput = this.defaultInputConf.getAttribute("name", this.defaultInput);
351 this.separator = config.getChild("separator").getValue(this.separator);
352 this.defaultPrefix = config.getChild("prefix").getValue(this.defaultPrefix);
353 this.defaultSuffix = config.getChild("suffix").getValue(this.defaultSuffix);
354 this.fixedName = config.getChild("fixed-attribute").getValue(this.fixedName);
355 this.useFormName = config.getChild("use-form-name").getValueAsBoolean(this.useFormName);
356 this.useFormNameTwice =
357 config.getChild("use-form-name-twice").getValueAsBoolean(this.useFormNameTwice);
358 this.useFormName = this.useFormName || this.useFormNameTwice;
359 if (this.useFormName) {
360 this.separator =
361 (this.separator == null || this.separator.length() == 0 ? "/" : this.separator);
362 this.defaultPrefix = this.separator;
363 }
364 this.ignoreValidation =
365 config.getChild("ignore-validation").getValueAsBoolean(this.ignoreValidation);
366 this.decorationSize = config.getChild("decoration").getValueAsInteger(this.decorationSize);
367 this.stripNumber = config.getChild("strip-number").getValueAsBoolean(this.stripNumber);
368 }
369
370 /**
371 * Read sitemap parameters and set properties accordingly.
372 */
373 private void evaluateParameters() {
374 this.documentFixed = this.parameters.getParameterAsBoolean("fixed", false);
375 this.fixed = this.documentFixed;
376 this.prefix = this.parameters.getParameter("prefix", this.defaultPrefix);
377 this.suffix = this.parameters.getParameter("suffix", this.defaultSuffix);
378 this.inputName = this.parameters.getParameter("input", null);
379 this.decorationSize =
380 this.parameters.getParameterAsInteger("decoration", this.decorationSize);
381 this.stripNumber = this.parameters.getParameterAsBoolean("strip-number", this.stripNumber);
382 }
383
384 /**
385 * Setup the next round.
386 * The instance variables are initialised.
387 * @param resolver The current SourceResolver
388 * @param objectModel The objectModel of the environment.
389 * @param src The value of the src attribute in the sitemap.
390 * @param par The parameters from the sitemap.
391 */
392 public void setup(SourceResolver resolver, Map objectModel, String src, Parameters par)
393 throws ProcessingException, SAXException, IOException {
394
395 this.reset();
396
397 super.setup(resolver, objectModel, src, par);
398
399 if (request == null) {
400 getLogger().debug("no request object");
401 throw new ProcessingException("no request object");
402 }
403 this.evaluateParameters();
404 this.setupInputModule();
405
406 }
407
408 /**
409 * Setup and obtain reference to the input module.
410 */
411 private void setupInputModule() {
412 this.inputConf = null;
413 if (this.ignoreValidation) {
414 this.validationResults = null;
415 } else {
416 this.validationResults = FormValidatorHelper.getResults(this.objectModel);
417 }
418
419 if (this.inputName == null) {
420 this.inputName = this.defaultInput;
421 this.inputConf = this.defaultInputConf;
422 }
423
424 try {
425 // obtain input module
426 this.inputSelector = (ServiceSelector) this.manager.lookup(INPUT_MODULE_SELECTOR);
427 if (this.inputName != null
428 && this.inputSelector != null
429 && this.inputSelector.isSelectable(this.inputName)) {
430 this.input = (InputModule) this.inputSelector.select(this.inputName);
431 if (!(this.input instanceof ThreadSafe
432 && this.inputSelector instanceof ThreadSafe)) {
433 this.inputSelector.release(this.input);
434 this.manager.release(this.inputSelector);
435 this.input = null;
436 this.inputSelector = null;
437 }
438 } else {
439 if (this.inputName != null)
440 if (getLogger().isErrorEnabled())
441 getLogger().error(
442 "A problem occurred setting up '"
443 + this.inputName
444 + "': Selector is "
445 + (this.inputSelector != null ? "not " : "")
446 + "null, Component is "
447 + (this.inputSelector != null
448 && this.inputSelector.isSelectable(this.inputName)
449 ? "known"
450 : "unknown"));
451 }
452 } catch (Exception e) {
453 if (getLogger().isWarnEnabled())
454 getLogger().warn(
455 "A problem occurred setting up '" + this.inputName + "': " + e.getMessage());
456 }
457 }
458
459 /**
460 * Recycle this component.
461 */
462 public void recycle() {
463 reset();
464 super.recycle();
465 }
466
467 /**
468 * Generate string representation of attributes. For debug only.
469 */
470 protected String printAttributes(Attributes attr) {
471 StringBuffer sb = new StringBuffer();
472 sb.append('[');
473 for (int i = 0; i < attr.getLength(); i++) {
474 sb.append('@').append(attr.getLocalName(i)).append("='").append(
475 attr.getValue(i)).append(
476 "' ");
477 }
478 sb.append(']');
479 return sb.toString();
480 }
481
482 /**
483 * Handle input elements that may have a "checked" attributes,
484 * i.e. checkbox and radio.
485 */
486 protected void startCheckableElement(
487 String aName,
488 String uri,
489 String name,
490 String raw,
491 AttributesImpl attributes)
492 throws SAXException {
493
494 // @fixed and this.fixed already considered in startInputElement
495 this.values = this.getValues(aName);
496 String checked = attributes.getValue("checked");
497 String value = attributes.getValue("value");
498 boolean found = false;
499
500 if (getLogger().isDebugEnabled())
501 getLogger().debug(
502 "startCheckableElement "
503 + name
504 + " attributes "
505 + this.printAttributes(attributes));
506 if (this.values != null) {
507 if (getLogger().isDebugEnabled())
508 getLogger().debug("replacing");
509 for (int i = 0; i < this.values.length; i++) {
510 if (this.values[i].equals(value)) {
511 found = true;
512 if (checked == null) {
513 attributes.addAttribute("", "checked", "checked", "CDATA", "");
514 }
515 break;
516 }
517 }
518 if (!found && checked != null) {
519 attributes.removeAttribute(attributes.getIndex("checked"));
520 }
521 }
522 this.relayStartElement(uri, name, raw, attributes);
523 }
524
525 /**
526 * Handle input elements that may don't have a "checked"
527 * attributes, e.g. text, password, button.
528 */
529 protected void startNonCheckableElement(
530 String aName,
531 String uri,
532 String name,
533 String raw,
534 AttributesImpl attributes)
535 throws SAXException {
536
537 // @fixed and this.fixed already considered in startInputElement
538 Object fValue = this.getNextValue(aName);
539 String value = attributes.getValue("value");
540 if (getLogger().isDebugEnabled())
541 getLogger().debug(
542 "startNonCheckableElement "
543 + name
544 + " attributes "
545 + this.printAttributes(attributes));
546 if (fValue != null) {
547 if (getLogger().isDebugEnabled())
548 getLogger().debug("replacing");
549 if (value != null) {
550 attributes.setValue(attributes.getIndex("value"), String.valueOf(fValue));
551 } else {
552 attributes.addAttribute("", "value", "value", "CDATA", String.valueOf(fValue));
553 }
554 }
555 this.relayStartElement(uri, name, raw, attributes);
556 }
557
558 /**
559 * Handle input elements. Calls startCheckableElement or
560 * startNonCheckableElement.
561 */
562 protected void startInputElement(String uri, String name, String raw, Attributes attr)
563 throws SAXException {
564
565 // @value = request.getParameterValues(@name)
566 String aName = getName(attr.getValue("name"));
567 String fixed = attr.getValue(this.fixedName);
568
569 if (getLogger().isDebugEnabled())
570 getLogger().debug(
571 "startInputElement " + name + " attributes " + this.printAttributes(attr));
572 if (aName == null || this.fixed || (fixed != null && BooleanUtils.toBoolean(fixed))) {
573 this.relayStartElement(uri, name, raw, attr);
574
575 } else {
576 if (getLogger().isDebugEnabled())
577 getLogger().debug("replacing");
578
579 attr = this.normalizeAttributes(attr);
580
581 AttributesImpl attributes = null;
582 if (attr instanceof AttributesImpl) {
583 attributes = (AttributesImpl) attr;
584 } else {
585 attributes = new AttributesImpl(attr);
586 }
587 String type = attributes.getValue("type");
588 switch (((Integer) inputTypes.get(type, defaultType)).intValue()) {
589 case TYPE_CHECKBOX :
590 case TYPE_RADIO :
591 this.startCheckableElement(aName, uri, name, raw, attributes);
592 break;
593
594 case TYPE_DEFAULT :
595 this.startNonCheckableElement(aName, uri, name, raw, attributes);
596 break;
597 }
598 this.values = null;
599 }
600 }
601
602 /**
603 * Handle select elements. Sets up some instance variables for
604 * following option elements.
605 */
606 protected void startSelectElement(String uri, String name, String raw, Attributes attr)
607 throws SAXException {
608
609 // this.values = request.getParameterValues(@name)
610 String aName = getName(attr.getValue("name"));
611 String fixed = attr.getValue(this.fixedName);
612 this.values = null;
613 if (getLogger().isDebugEnabled())
614 getLogger().debug(
615 "startSelectElement " + name + " attributes " + this.printAttributes(attr));
616 if (aName != null && !(this.fixed || (fixed != null && BooleanUtils.toBoolean(fixed)))) {
617 if (attr.getIndex("multiple") > -1) {
618 this.values = this.getValues(aName);
619 } else {
620 Object val = this.getNextValue(aName);
621 if (val != null) {
622 this.values = new Object[1];
623 this.values[0] = val;
624 } else {
625 this.values = null;
626 }
627 }
628 attr = this.normalizeAttributes(attr);
629 }
630 this.relayStartElement(uri, name, raw, attr);
631 }
632
633 /**
634 * Handle option elements. Uses instance variables set up by
635 * startSelectElement. Relies on option having a "value"
636 * attribute, i.e. does not check following characters if "value"
637 * is not present.
638 */
639 protected void startOptionElement(String uri, String name, String raw, Attributes attr)
640 throws SAXException {
641
642 // add @selected if @value in request.getParameterValues(@name)
643 if (getLogger().isDebugEnabled())
644 getLogger().debug(
645 "startOptionElement " + name + " attributes " + this.printAttributes(attr));
646 if (this.values == null || this.fixed) {
647 this.relayStartElement(uri, name, raw, attr);
648 } else {
649 if (getLogger().isDebugEnabled())
650 getLogger().debug("replacing");
651 AttributesImpl attributes = null;
652 if (attr instanceof AttributesImpl) {
653 attributes = (AttributesImpl) attr;
654 } else {
655 attributes = new AttributesImpl(attr);
656 }
657 String selected = attributes.getValue("selected");
658 String value = attributes.getValue("value");
659 boolean found = false;
660
661 for (int i = 0; i < this.values.length; i++) {
662 if (this.values[i].equals(value)) {
663 found = true;
664 if (selected == null) {
665 attributes.addAttribute("", "selected", "selected", "CDATA", "");
666 }
667 break;
668 }
669 }
670 if (!found && selected != null) {
671 attributes.removeAttribute(attributes.getIndex("selected"));
672 }
673
674 this.relayStartElement(uri, name, raw, attributes);
675 }
676 }
677
678 /**
679 * Handles textarea elements. Skips nested events if request
680 * parameter with same name exists.
681 */
682 protected void startTextareaElement(String uri, String name, String raw, Attributes attributes)
683 throws SAXException {
684
685 String aName = getName(attributes.getValue("name"));
686 String fixed = attributes.getValue(this.fixedName);
687 Object value = null;
688 if (getLogger().isDebugEnabled())
689 getLogger().debug(
690 "startTextareaElement " + name + " attributes " + this.printAttributes(attributes));
691 if (aName != null) {
692 value = this.getNextValue(aName);
693 }
694 if (value == null || this.fixed || (fixed != null && BooleanUtils.toBoolean(fixed))) {
695 this.relayStartElement(uri, name, raw, attributes);
696 } else {
697 if (getLogger().isDebugEnabled())
698 getLogger().debug("replacing");
699 this.relayStartElement(uri, name, raw, this.normalizeAttributes(attributes));
700 String valString = String.valueOf(value);
701 this.characters(valString.toCharArray(), 0, valString.length());
702 // well, this doesn't really work out nicely. do it the hard way.
703 if (this.ignoreEventsCount == 0)
704 this.skipChildrenOnly = true;
705 this.ignoreEventsCount++;
706 }
707 }
708
709 /**
710 * Handle error elements. If validation results are available,
711 * compares validation result for parameter with the same name as
712 * the "name" attribute with the result names is "when" and
713 * "when-ge". Drops element and all nested events when error
714 * condition is not met.
715 */
716 protected void startErrorElement(String uri, String name, String raw, Attributes attr)
717 throws SAXException {
718
719 if (getLogger().isDebugEnabled())
720 getLogger().debug(
721 "startErrorElement " + name + " attributes " + this.printAttributes(attr));
722 if (this.ignoreValidation) {
723 this.relayStartElement(uri, name, raw, attr);
724 } else if (this.validationResults == null || this.fixed) {
725 this.relayStartElement(true, false, uri, name, raw, attr);
726 } else {
727 String aName = attr.getValue("name");
728 if (aName == null) {
729 this.relayStartElement(uri, name, raw, attr);
730 } else {
731 ValidatorActionResult validation =
732 FormValidatorHelper.getParamResult(this.objectModel, aName);
733 String when = attr.getValue("when");
734 String when_ge = attr.getValue("when-ge");
735
736 if ((when != null && when.equals(validatorResults.get(validation)))
737 || (when_ge != null
738 && validation.ge(
739 (ValidatorActionResult) validatorResultLabel.get(
740 when_ge,
741 ValidatorActionResult.MAXERROR)))) {
742 AttributesImpl attributes = null;
743 if (attr instanceof AttributesImpl) {
744 attributes = (AttributesImpl) attr;
745 } else {
746 attributes = new AttributesImpl(attr);
747 }
748 // remove attributes not meant for client
749 attributes.removeAttribute(attributes.getIndex("name"));
750 if (when != null)
751 attributes.removeAttribute(attributes.getIndex("when"));
752 if (when_ge != null)
753 attributes.removeAttribute(attributes.getIndex("when-ge"));
754 this.relayStartElement(uri, name, raw, this.normalizeAttributes(attributes));
755 } else {
756 this.relayStartElement(true, true, uri, name, raw, attr);
757 }
758 }
759 }
760 }
761
762 /**
763 * Start processing a form element. Sets protection indicator if attribute
764 * "fixed" is present and either "true" or "yes". Removes attribute "fixed"
765 * if present.
766 * @param uri The namespace of the element.
767 * @param name The local name of the element.
768 * @param raw The qualified name of the element.
769 * @param attr The attributes of the element.
770 */
771 protected void startFormElement(String uri, String name, String raw, Attributes attr)
772 throws SAXException {
773
774 String fixed = attr.getValue(this.fixedName);
775 if (this.useFormName) {
776 this.formName = attr.getValue("name");
777 }
778 if (fixed == null) {
779 this.relayStartElement(uri, name, raw, attr);
780 } else {
781 if (!this.fixed && BooleanUtils.toBoolean(fixed)) {
782 this.fixed = true;
783 }
784 // remove attributes not meant for client
785 AttributesImpl attributes = null;
786 if (attr instanceof AttributesImpl) {
787 attributes = (AttributesImpl) attr;
788 } else {
789 attributes = new AttributesImpl(attr);
790 }
791 attributes.removeAttribute(attributes.getIndex(this.fixedName));
792 this.relayStartElement(uri, name, raw, this.normalizeAttributes(attributes));
793 }
794 }
795
796 /**
797 * Start recording repeat element contents and push repeat expression and
798 * variable to repeater stack. Only start recording, if no other recorder is
799 * currently running.
800 *
801 * @param uri
802 * @param name
803 * @param raw
804 * @param attr
805 * @throws SAXException
806 */
807 protected void startRepeatElement(String uri, String name, String raw, Attributes attr)
808 throws SAXException {
809
810 if (this.recordingCount == 0) {
811 if (!(this.fixed || BooleanUtils.toBoolean(attr.getValue(this.fixedName)))) {
812 RepeaterStatus status =
813 new RepeaterStatus("${" + attr.getValue("using") + "}", 0, attr.getValue("on"));
814 this.repeater.add(status);
815 this.startRecording();
816 this.recordingCount++;
817 } else {
818 this.relayStartElement(uri, name, raw, attr);
819 }
820 } else {
821 this.relayStartElement(uri, name, raw, attr);
822 this.recordingCount++;
823 }
824 }
825
826 /**
827 * Stop recording repeat contents and replay required number of times.
828 * Stop only if outmost repeat element is ending.
829 *
830 * @param uri
831 * @param name
832 * @param raw
833 * @throws SAXException
834 */
835 protected void endRepeatElement(String uri, String name, String raw) throws SAXException {
836 this.recordingCount--;
837 if (this.recordingCount == 0) {
838 DocumentFragment fragment = this.endRecording();
839 RepeaterStatus status = (RepeaterStatus) this.repeater.get(this.repeater.size() - 1);
840 Object[] vals = this.getValues(this.getName(status.expr));
841 int count = (vals != null ? vals.length : 0);
842 for (status.count = 1; status.count <= count; status.count++) {
843 DOMStreamer streamer = new DOMStreamer(this, this);
844 streamer.stream(fragment);
845 }
846 this.repeater.remove(this.repeater.size() - 1);
847 } else {
848 this.relayEndElement(uri, name, raw);
849 if (this.recordingCount < 0) {
850 this.recordingCount = 0;
851 }
852 }
853 }
854
855 /**
856 * Start processing elements of our namespace.
857 * This hook is invoked for each sax event with our namespace.
858 * @param uri The namespace of the element.
859 * @param name The local name of the element.
860 * @param raw The qualified name of the element.
861 * @param attr The attributes of the element.
862 */
863 public void startTransformingElement(String uri, String name, String raw, Attributes attr)
864 throws SAXException {
865
866 if (this.ignoreEventsCount == 0 && this.recordingCount == 0) {
867 switch (((Integer) elementNames.get(name, defaultElement)).intValue()) {
868 case ELEMENT_INPUT :
869 this.startInputElement(uri, name, raw, attr);
870 break;
871
872 case ELEMENT_SELECT :
873 this.startSelectElement(uri, name, raw, attr);
874 break;
875
876 case ELEMENT_OPTION :
877 this.startOptionElement(uri, name, raw, attr);
878 break;
879
880 case ELEMENT_TXTAREA :
881 this.startTextareaElement(uri, name, raw, attr);
882 break;
883
884 case ELEMENT_ERROR :
885 this.startErrorElement(uri, name, raw, attr);
886 break;
887
888 case ELEMENT_FORM :
889 this.startFormElement(uri, name, raw, attr);
890 break;
891
892 case ELEMENT_REPEAT :
893 this.startRepeatElement(uri, name, raw, attr);
894 break;
895
896 default :
897 this.relayStartElement(uri, name, raw, attr);
898 }
899
900 } else if (this.recordingCount > 0) {
901 switch (((Integer) elementNames.get(name, defaultElement)).intValue()) {
902 case ELEMENT_REPEAT :
903 this.startRepeatElement(uri, name, raw, attr);
904 break;
905
906 default :
907 this.relayStartElement(uri, name, raw, attr);
908 }
909 } else {
910 this.relayStartElement(uri, name, raw, attr);
911 }
912 }
913
914 /**
915 * Start processing elements of our namespace.
916 * This hook is invoked for each sax event with our namespace.
917 * @param uri The namespace of the element.
918 * @param name The local name of the element.
919 * @param raw The qualified name of the element.
920 */
921 public void endTransformingElement(String uri, String name, String raw) throws SAXException {
922
923 if (this.ignoreEventsCount > 0) {
924 this.relayEndElement(uri, name, raw);
925 } else if (this.recordingCount > 0) {
926 switch (((Integer) elementNames.get(name, defaultElement)).intValue()) {
927 case ELEMENT_REPEAT :
928 this.endRepeatElement(uri, name, raw);
929 break;
930
931 default :
932 this.relayEndElement(uri, name, raw);
933 }
934 } else {
935 switch (((Integer) elementNames.get(name, defaultElement)).intValue()) {
936 case ELEMENT_SELECT :
937 this.values = null;
938 this.relayEndElement(uri, name, raw);
939 break;
940 case ELEMENT_INPUT :
941 case ELEMENT_OPTION :
942 case ELEMENT_TXTAREA :
943 case ELEMENT_ERROR :
944 this.relayEndElement(uri, name, raw);
945 break;
946 case ELEMENT_FORM :
947 this.fixed = this.documentFixed;
948 this.formName = null;
949 this.relayEndElement(uri, name, raw);
950 break;
951
952 case ELEMENT_REPEAT :
953 this.endRepeatElement(uri, name, raw);
954 break;
955
956 default :
957 this.relayEndElement(uri, name, raw);
958 }
959 }
960 }
961
962 /**
963 * Remove extra information from element's attributes. Currently only removes
964 * the repeater variable from the element's name attribute if present.
965 *
966 * @param attr
967 * @return modified attributes
968 */
969 private Attributes normalizeAttributes(Attributes attr) {
970 Attributes result = attr;
971 if (this.stripNumber && this.repeater.size() > 0) {
972 String name = attr.getValue("name");
973 if (name != null) {
974 for (Iterator i = this.repeater.iterator(); i.hasNext();) {
975 RepeaterStatus status = (RepeaterStatus) i.next();
976 int pos = name.indexOf(status.var);
977 if (pos >= 0) {
978 AttributesImpl attributes;
979 if (result instanceof AttributesImpl) {
980 attributes = (AttributesImpl) result;
981 } else {
982 attributes = new AttributesImpl(result);
983 }
984 name =
985 name.substring(0, pos - this.decorationSize)
986 + name.substring(pos + status.var.length() + this.decorationSize);
987 attributes.setValue(attributes.getIndex("name"), name);
988 result = attributes;
989 }
990 }
991 }
992 }
993 return result;
994 }
995
996 /**
997 * Generate the "real" name of an element for value lookup.
998 * @param name
999 * @return "real" name.
1000 */
1001 private String getName(String name) {
1002 String result = name;
1003 if (this.useFormName && this.formName != null) {
1004 if (this.separator != null) {
1005 if (this.useFormNameTwice) {
1006 result =
1007 this.formName + this.separator + this.formName + this.separator + result;
1008 } else {
1009 result = this.formName + this.separator + result;
1010 }
1011 } else {
1012 if (this.useFormNameTwice) {
1013 result = this.formName + result;
1014 } else {
1015 // does this make sense ?
1016 result = this.formName + this.formName + result;
1017 }
1018 }
1019 }
1020 if (this.prefix != null) {
1021 result = this.prefix + result;
1022 }
1023 if (this.suffix != null) {
1024 result = result + this.prefix;
1025 }
1026 if (this.repeater.size() > 0) {
1027 for (Iterator i = this.repeater.iterator(); i.hasNext();) {
1028 RepeaterStatus status = (RepeaterStatus) i.next();
1029 int pos = result.indexOf(status.var);
1030 if (pos != -1) {
1031 result =
1032 result.substring(0, pos)
1033 + status.count
1034 + result.substring(pos + status.var.length());
1035 }
1036 }
1037 }
1038 return result;
1039 }
1040
1041 /**
1042 * Obtain values from used InputModule if not done already and return the
1043 * next value. If no more values exist, returns null.
1044 *
1045 * @param name
1046 */
1047 private Object getNextValue(String name) {
1048 Object result = null;
1049 if (this.formValues.containsKey(name)) {
1050 ValueList vList = (ValueList) this.formValues.get(name);
1051 result = vList.getNext();
1052 } else {
1053 ValueList vList = new ValueList(this.getValues(name));
1054 result = vList.getNext();
1055 this.formValues.put(name, vList);
1056 }
1057 return result;
1058 }
1059
1060 /**
1061 * Obtain values from the used InputModule.
1062 */
1063 private Object[] getValues(String name) {
1064 Object[] values = null;
1065 ServiceSelector iputSelector = null;
1066 InputModule iput = null;
1067 try {
1068 if (this.input != null) {
1069 // input module is thread safe
1070 // thus we still have a reference to it
1071 values = input.getAttributeValues(name, this.inputConf, objectModel);
1072 if (getLogger().isDebugEnabled())
1073 getLogger().debug("cached module " + this.input
1074 + " attribute " + name
1075 + " returns " + values);
1076 } else {
1077 // input was not thread safe
1078 // so acquire it again
1079 iputSelector = (ServiceSelector)this.manager.lookup(INPUT_MODULE_SELECTOR);
1080 if (this.inputName != null
1081 && iputSelector != null
1082 && iputSelector.isSelectable(this.inputName)) {
1083
1084 iput = (InputModule) iputSelector.select(this.inputName);
1085 }
1086 if (iput != null) {
1087 values = iput.getAttributeValues(name, this.inputConf, objectModel);
1088 }
1089 if (getLogger().isDebugEnabled())
1090 getLogger().debug(
1091 "fresh module " + iput + " attribute " + name + " returns " + values);
1092 }
1093 } catch (Exception e) {
1094 if (getLogger().isWarnEnabled())
1095 getLogger().warn(
1096 "A problem occurred acquiring a value from '"
1097 + this.inputName
1098 + "' for '"
1099 + name
1100 + "': "
1101 + e.getMessage());
1102 } finally {
1103 // release components if necessary
1104 if (iputSelector != null) {
1105 if (iput != null)
1106 iputSelector.release(iput);
1107 this.manager.release(iputSelector);
1108 }
1109 }
1110
1111 return values;
1112 }
1113
1114 /**
1115 * Calls the super's method startTransformingElement.
1116 *
1117 * @param uri
1118 * @param name
1119 * @param raw
1120 * @param attr
1121 * @throws SAXException
1122 */
1123 protected void relayStartElement(String uri, String name, String raw, Attributes attr)
1124 throws SAXException {
1125 this.relayStartElement(false, false, uri, name, raw, attr);
1126 }
1127
1128 /**
1129 * Calls the super's method startTransformingElement and increments the
1130 * ignoreEventsCount if skip is true. Increment can be done either before
1131 * invoking super's method, so that the element itself is skipped, or afterwards,
1132 * so that only the children are skipped.
1133 *
1134 * @param skip
1135 * @param skipChildrenOnly
1136 * @param uri
1137 * @param name
1138 * @param raw
1139 * @param attr
1140 * @throws SAXException
1141 */
1142 protected void relayStartElement(
1143 boolean skip,
1144 boolean skipChildrenOnly,
1145 String uri,
1146 String name,
1147 String raw,
1148 Attributes attr)
1149 throws SAXException {
1150
1151 try {
1152 if (this.ignoreEventsCount > 0) {
1153 this.ignoreEventsCount++;
1154 super.startTransformingElement(uri, name, raw, attr);
1155 } else {
1156 if (skip)
1157 this.skipChildrenOnly = skipChildrenOnly;
1158 if (skip && !skipChildrenOnly)
1159 this.ignoreEventsCount++;
1160 super.startTransformingElement(uri, name, raw, attr);
1161 if (skip && skipChildrenOnly)
1162 this.ignoreEventsCount++;
1163 }
1164 } catch (ProcessingException e) {
1165 throw new SAXException(e);
1166 } catch (IOException e) {
1167 throw new SAXException(e);
1168 }
1169 }
1170
1171 /**
1172 * Calls the super's method endTransformingElement and decrements the
1173 * ignoreEventsCount if larger than zero.
1174 *
1175 * @param uri
1176 * @param name
1177 * @param raw
1178 * @throws SAXException
1179 */
1180 protected void relayEndElement(String uri, String name, String raw) throws SAXException {
1181
1182 if (this.ignoreEventsCount == 1 && this.skipChildrenOnly)
1183 this.ignoreEventsCount--;
1184 try {
1185 super.endTransformingElement(uri, name, raw);
1186 } catch (ProcessingException e) {
1187 throw new SAXException(e);
1188 } catch (IOException e) {
1189 throw new SAXException(e);
1190 } catch (Exception e) {
1191 getLogger().error("exception", e);
1192 }
1193
1194 if (this.ignoreEventsCount > 0)
1195 this.ignoreEventsCount--;
1196 }
1197
1198 }