001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.forms.formmodel;
018:
019: import java.util.Locale;
020:
021: import org.apache.cocoon.environment.Request;
022: import org.apache.cocoon.forms.FormContext;
023: import org.apache.cocoon.forms.FormsConstants;
024: import org.apache.cocoon.forms.FormsRuntimeException;
025: import org.apache.cocoon.forms.datatype.Datatype;
026: import org.apache.cocoon.forms.datatype.SelectionList;
027: import org.apache.cocoon.forms.datatype.convertor.ConversionResult;
028: import org.apache.cocoon.forms.event.DeferredValueChangedEvent;
029: import org.apache.cocoon.forms.event.ValueChangedEvent;
030: import org.apache.cocoon.forms.event.ValueChangedListener;
031: import org.apache.cocoon.forms.event.ValueChangedListenerEnabled;
032: import org.apache.cocoon.forms.event.WidgetEvent;
033: import org.apache.cocoon.forms.event.WidgetEventMulticaster;
034: import org.apache.cocoon.forms.util.I18nMessage;
035: import org.apache.cocoon.forms.validation.ValidationError;
036: import org.apache.cocoon.forms.validation.ValidationErrorAware;
037: import org.apache.cocoon.xml.AttributesImpl;
038: import org.apache.cocoon.xml.XMLUtils;
039:
040: import org.apache.commons.lang.ObjectUtils;
041: import org.apache.commons.lang.StringUtils;
042: import org.xml.sax.ContentHandler;
043: import org.xml.sax.SAXException;
044:
045: /**
046: * A general-purpose Widget that can hold one value. A Field widget can be associated
047: * with a {@link org.apache.cocoon.forms.datatype.Datatype Datatype}, and thus
048: * a Field widget can be used to edit different kinds of data, such as strings,
049: * numbers and dates. A Datatype can also have an associated SelectionList, so
050: * that the value for the Field can be selected from a list, rather than being
051: * entered in a textbox. The validation of the field is delegated to its associated
052: * Datatype.
053: *
054: * @version $Id: Field.java 474132 2006-11-13 04:07:30Z jjohnston $
055: */
056: public class Field extends AbstractWidget implements
057: ValidationErrorAware, DataWidget, SelectableWidget,
058: ValueChangedListenerEnabled {
059:
060: /**
061: * If the field was rendered as a suggestion-list and the user chose one of the suggestions,
062: * the field's value is the chosen item's value and the <code>SUGGESTED_LABEL_ATTR</code> field
063: * attribute contains the chosen item's label.
064: *
065: * @see #isSuggested()
066: * @since 2.1.9
067: */
068: public static final String SUGGESTED_LABEL_ATTR = "suggested-label";
069:
070: /**
071: * Value state indicating that a new value has been read from the request,
072: * but has not yet been parsed.
073: */
074: protected static final int VALUE_UNPARSED = 0;
075:
076: /**
077: * Value state indicating that a value has been parsed, but needs to be
078: * validated (that must occur before the value is given to the application)
079: */
080: protected static final int VALUE_PARSED = 1;
081:
082: /**
083: * Value state indicating that a parse error was encountered but should not
084: * yet be displayed.
085: */
086: protected static final int VALUE_PARSE_ERROR = 2;
087:
088: /**
089: * Value state indicating that validate() has been called when state was
090: * VALUE_PARSE_ERROR. This makes the error visible on output.
091: */
092: protected static final int VALUE_DISPLAY_PARSE_ERROR = 3;
093:
094: /**
095: * Transient value state indicating that validation is going on.
096: *
097: * @see #validate()
098: */
099: protected static final int VALUE_VALIDATING = 4;
100:
101: /**
102: * Value state indicating that validation has occured, but that any error should not
103: * yet be displayed.
104: */
105: protected static final int VALUE_VALIDATED = 5;
106:
107: /**
108: * Value state indicating that value validation has occured, and the
109: * validation error, if any, should be displayed.
110: */
111: protected static final int VALUE_DISPLAY_VALIDATION = 6;
112:
113: private static final String FIELD_EL = "field";
114: private static final String VALUE_EL = "value";
115: private static final String VALIDATION_MSG_EL = "validation-message";
116:
117: /**
118: * Definition of the field.
119: */
120: private final FieldDefinition definition;
121:
122: /**
123: * Overrides selection list defined in FieldDefinition, if any.
124: */
125: protected SelectionList selectionList;
126:
127: /**
128: * Additional listeners to those defined as part of the widget definition (if any).
129: */
130: private ValueChangedListener listener;
131:
132: protected String enteredValue;
133: protected Object value;
134:
135: protected boolean required;
136:
137: /**
138: * Transient widget processing state indicating that the widget is currently validating
139: * (used to avoid endless loops when a validator calls getValue).
140: */
141: protected int valueState = VALUE_PARSED;
142:
143: protected ValidationError validationError;
144:
145: public Field(FieldDefinition fieldDefinition) {
146: super (fieldDefinition);
147:
148: this .definition = fieldDefinition;
149: this .listener = fieldDefinition.getValueChangedListener();
150: /*
151: * At startup, we have no value to parse (both enteredValue and value are null),
152: * but still need to validate (e.g. error if field is required), so initial value
153: * is set to {@link #VALUE_PARSED}.
154: */
155: this .valueState = VALUE_PARSED;
156: }
157:
158: public WidgetDefinition getDefinition() {
159: return this .definition;
160: }
161:
162: public final FieldDefinition getFieldDefinition() {
163: return this .definition;
164: }
165:
166: public void initialize() {
167: Object value = this .definition.getInitialValue();
168: if (value != null) {
169: setValue(value);
170: }
171: this .selectionList = this .definition.getSelectionList();
172: this .required = this .definition.isRequired();
173: super .initialize();
174: }
175:
176: /**
177: * If this field has a selection-list, indicates if the value comes from that list
178: * or if a new value was input by the user.
179: *
180: * @since 2.1.9
181: * @return true if the user has chosen a suggested value
182: */
183: public boolean isSuggested() {
184: return this .getAttribute(SUGGESTED_LABEL_ATTR) != null;
185: }
186:
187: /**
188: * Set the suggestion label associated to the widget's current value. This is used to initialize
189: * a combobox's rendering. If not such label exists, the widget's value is used.
190: *
191: * @since 2.1.9
192: */
193: public void setSuggestionLabel(String label) {
194: if (this .definition.getSuggestionList() == null) {
195: throw new FormsRuntimeException("Field '"
196: + getRequestParameterName()
197: + "' has no suggestion list.", getLocation());
198: }
199: setAttribute(SUGGESTED_LABEL_ATTR, label);
200: }
201:
202: /**
203: * If the user has chosen an item in a suggestion list, returns that item's label.
204: *
205: * @since 2.1.9
206: * @return the item's label, or <code>null</code> if the user entered a new value or
207: * if there's not suggestion list.
208: */
209: public String getSuggestionLabel() {
210: return (String) getAttribute(SUGGESTED_LABEL_ATTR);
211: }
212:
213: public Object getValue() {
214: // if getValue() is called on this field while we're validating, then it's because a validation
215: // rule called getValue(), so then we just return the parsed (but not VALUE_VALIDATED) value to avoid an endless loop
216: if (this .valueState == VALUE_VALIDATING) {
217: return this .value;
218: }
219:
220: ValidationError oldError = this .validationError;
221:
222: // Parse the value
223: if (this .valueState == VALUE_UNPARSED) {
224: doParse();
225: }
226:
227: // Validate the value if it was successfully parsed
228: if (this .valueState == VALUE_PARSED) {
229: doValidate();
230: }
231:
232: if (oldError != null && this .validationError == null) {
233: // The parsing process removed an existing validation error. This happens
234: // mainly when a required field is given a value.
235: getForm().addWidgetUpdate(this );
236: }
237:
238: return this .validationError == null ? this .value : null;
239: }
240:
241: public void setValue(Object newValue) {
242: if (newValue != null
243: && !getDatatype().getTypeClass().isAssignableFrom(
244: newValue.getClass())) {
245: throw new FormsRuntimeException(
246: "Incorrect value type for '"
247: + getRequestParameterName()
248: + "'. Expected "
249: + getDatatype().getTypeClass() + ", got "
250: + newValue.getClass() + ").", getLocation());
251: }
252:
253: // Is it a new value?
254: boolean changed;
255: if (this .valueState == VALUE_UNPARSED) {
256: // Current value was not parsed
257: changed = true;
258: } else if (this .value == null) {
259: // Is current value not null?
260: changed = (newValue != null);
261: } else {
262: // Is current value different?
263: changed = !this .value.equals(newValue);
264: }
265:
266: // Do something only if value is different or null
267: // (null allows to reset validation error)
268: if (changed || newValue == null) {
269: // Do we need to call listeners? If yes, keep (and parse if needed) old value.
270: boolean callListeners = changed
271: && (hasValueChangedListeners() || this .getForm()
272: .hasFormHandler());
273: Object oldValue = callListeners ? getValue() : null;
274:
275: this .value = newValue;
276: this .validationError = null;
277: // Force validation, even if set by the application
278: this .valueState = VALUE_PARSED;
279: if (newValue != null) {
280: this .enteredValue = getDatatype().convertToString(
281: newValue, getForm().getLocale());
282: } else {
283: this .enteredValue = null;
284: }
285:
286: if (callListeners) {
287: getForm()
288: .addWidgetEvent(
289: new ValueChangedEvent(this , oldValue,
290: newValue));
291: }
292: getForm().addWidgetUpdate(this );
293: }
294: }
295:
296: public void readFromRequest(FormContext formContext) {
297: if (!getCombinedState().isAcceptingInputs()) {
298: return;
299: }
300:
301: String paramName = getRequestParameterName();
302: Request request = formContext.getRequest();
303:
304: String newEnteredValue = request.getParameter(paramName);
305:
306: if (this .definition.getSuggestionList() != null) {
307: // The Dojo ComboBox sends the typed value or the chosen item's label in the
308: // request parameter and sends an additional "*_selected" parameter containing
309: // the value of the chosen item (if any).
310: // So if *_selected exists, use
311: String selectedValue = request.getParameter(paramName
312: + "_selected");
313: if (StringUtils.isNotEmpty(selectedValue)) {
314: setSuggestionLabel(newEnteredValue);
315: newEnteredValue = selectedValue;
316: } else {
317: this .removeAttribute(SUGGESTED_LABEL_ATTR);
318: }
319: }
320:
321: // FIXME: Should we consider only non-null values?
322: // Although distinguishing an empty value (input present but blank) from a null value
323: // (input not present in the form) is possible, this distinction is not possible for
324: // several other kinds of widgets such as BooleanField or MultiValueField. So we keep
325: // it consistent with other widgets.
326: //if (newEnteredValue != null) {
327: readFromRequest(newEnteredValue);
328: //}
329: }
330:
331: protected void readFromRequest(String newEnteredValue) {
332: // whitespace & empty field handling
333: newEnteredValue = applyWhitespaceTrim(newEnteredValue);
334:
335: // Only convert if the text value actually changed. Otherwise, keep the old value
336: // and/or the old validation error (allows to keep errors when clicking on actions)
337: boolean changed;
338: if (enteredValue == null) {
339: changed = (newEnteredValue != null);
340: } else {
341: changed = !enteredValue.equals(newEnteredValue);
342: }
343:
344: if (changed) {
345: ValidationError oldError = this .validationError;
346:
347: // If we have some value-changed listeners, we must make sure the current value has been
348: // parsed, to fill the event. Otherwise, we don't need to spend that extra CPU time.
349: boolean hasListeners = hasValueChangedListeners()
350: || this .getForm().hasFormHandler();
351: Object oldValue = hasListeners ? getValue() : null;
352:
353: enteredValue = newEnteredValue;
354: validationError = null;
355: value = null;
356: this .valueState = VALUE_UNPARSED;
357:
358: if (hasListeners) {
359: // Throw an event that will hold the old value and
360: // will lazily compute the new value only if needed.
361: getForm().addWidgetEvent(
362: new DeferredValueChangedEvent(this , oldValue));
363: }
364:
365: if (oldError != null) {
366: // There was a validation error, and the user entered a new value: refresh
367: // the widget, because the previous error was cleared
368: getForm().addWidgetUpdate(this );
369: }
370: }
371: }
372:
373: protected String applyWhitespaceTrim(String value) {
374: if (value != null) {
375: Whitespace trim = this .definition.getWhitespaceTrim();
376: if (trim == null || trim == Whitespace.TRIM) {
377: value = value.trim();
378: } else if (trim == Whitespace.PRESERVE) {
379: // do nothing.
380: } else if (trim == Whitespace.TRIM_START) {
381: value = StringUtils.stripStart(value, null);
382: } else if (trim == Whitespace.TRIM_END) {
383: value = StringUtils.stripEnd(value, null);
384: }
385:
386: // treat empty strings as null
387: if (value.length() == 0) {
388: value = null;
389: }
390: }
391: return value;
392: }
393:
394: /**
395: * @see org.apache.cocoon.forms.formmodel.Widget#validate()
396: */
397: public boolean validate() {
398: if (!getCombinedState().isValidatingValues()) {
399: this .wasValid = true;
400: return true;
401: }
402:
403: if (this .valueState == VALUE_UNPARSED) {
404: doParse();
405: }
406:
407: // Force validation on already validated values (but keep invalid parsings)
408: if (this .valueState >= VALUE_VALIDATED) {
409: this .valueState = VALUE_PARSED;
410: }
411:
412: if (this .valueState == VALUE_PARSED) {
413: doValidate();
414: this .valueState = VALUE_DISPLAY_VALIDATION;
415: if (this .validationError != null) {
416: getForm().addWidgetUpdate(this );
417: }
418: } else if (this .valueState == VALUE_PARSE_ERROR) {
419: this .valueState = VALUE_DISPLAY_PARSE_ERROR;
420: getForm().addWidgetUpdate(this );
421: }
422:
423: this .wasValid = this .validationError == null;
424: return this .wasValid;
425: }
426:
427: /**
428: * Parse the value that has been read from the request.
429: * Should be called when valueState is VALUE_UNPARSED.
430: * On exit, valueState is set to either:
431: * - VALUE_PARSED: successful parsing or null value. Value is set and ValidationError
432: * is cleared.
433: * - VALUE_PARSE_ERROR: datatype parsing error. In that case, value is null and
434: * validationError is set.
435: */
436: private void doParse() {
437: if (this .valueState != VALUE_UNPARSED) {
438: throw new IllegalStateException(
439: "Field is not in UNPARSED state ("
440: + this .valueState + ")");
441: }
442:
443: // Clear value, it will be recomputed
444: this .value = null;
445: this .validationError = null;
446:
447: if (this .enteredValue != null) {
448: // Parse the value
449: ConversionResult conversionResult = getDatatype()
450: .convertFromString(this .enteredValue,
451: getForm().getLocale());
452: if (conversionResult.isSuccessful()) {
453: this .value = conversionResult.getResult();
454: this .valueState = VALUE_PARSED;
455: } else {
456: // Conversion failed
457: this .validationError = conversionResult
458: .getValidationError();
459: // No need for further validation (and need to keep the above error)
460: this .valueState = VALUE_PARSE_ERROR;
461: }
462: } else {
463: // No value: needs to be validated
464: this .valueState = VALUE_PARSED;
465: }
466: }
467:
468: /**
469: * Validate the value once it has been parsed.
470: * Should be called when valueState is VALUE_PARSED.
471: * On exit, valueState is set to VALUE_VALIDATED, and validationError is set if
472: * validation failed.
473: */
474: private void doValidate() {
475: if (this .valueState != VALUE_PARSED) {
476: throw new IllegalStateException(
477: "Field is not in PARSED state (" + this .valueState
478: + ")");
479: }
480:
481: // Go to transient validating state
482: this .valueState = VALUE_VALIDATING;
483:
484: // reset validation errot
485: this .validationError = null;
486:
487: try {
488: if (this .value == null && this .required) {
489: // Field is required
490: this .validationError = new ValidationError(
491: new I18nMessage("general.field-required",
492: FormsConstants.I18N_CATALOGUE));
493: } else if (!super .validate()) {
494: // New-style validators failed.
495: } else if (this .value != null) {
496: // Check the old-style ones.
497: this .validationError = getDatatype().validate(
498: this .value, new ExpressionContextImpl(this ));
499: }
500: } finally {
501: // Consider validation finished even in case of exception
502: this .valueState = VALUE_VALIDATED;
503: }
504: }
505:
506: /**
507: * Returns the validation error, if any. There will always be a validation error in case the
508: * {@link #validate} method returned false.
509: *
510: * <br>This method does not cause parsing to take effect, use {@link #getValue} if value
511: * is not parsed yet.
512: */
513: public ValidationError getValidationError() {
514: return this .validationError;
515: }
516:
517: /**
518: * Set a validation error on this field. This allows fields to be externally marked as invalid by
519: * application logic.
520: *
521: * @param error the validation error
522: */
523: public void setValidationError(ValidationError error) {
524: if (this .valueState >= VALUE_VALIDATED) {
525: this .valueState = VALUE_DISPLAY_VALIDATION;
526: }
527:
528: if (!ObjectUtils.equals(this .validationError, error)) {
529: this .validationError = error;
530: getForm().addWidgetUpdate(this );
531: }
532: }
533:
534: public boolean isRequired() {
535: return this .required;
536: }
537:
538: public void setRequired(boolean required) {
539: this .required = required;
540: getForm().addWidgetUpdate(this );
541: }
542:
543: /**
544: * @return "field"
545: */
546: public String getXMLElementName() {
547: return FIELD_EL;
548: }
549:
550: /**
551: * Adds the @required attribute
552: */
553: public AttributesImpl getXMLElementAttributes() {
554: AttributesImpl attrs = super .getXMLElementAttributes();
555: attrs.addCDATAAttribute("required", String
556: .valueOf(isRequired()));
557: return attrs;
558: }
559:
560: public void generateItemSaxFragment(ContentHandler contentHandler,
561: Locale locale) throws SAXException {
562: if (locale == null) {
563: locale = getForm().getLocale();
564: }
565:
566: if (enteredValue != null || value != null) {
567: contentHandler.startElement(FormsConstants.INSTANCE_NS,
568: VALUE_EL, FormsConstants.INSTANCE_PREFIX_COLON
569: + VALUE_EL, XMLUtils.EMPTY_ATTRIBUTES);
570: String stringValue;
571: if (value != null) {
572: stringValue = getDatatype().convertToString(value,
573: locale);
574: } else {
575: stringValue = enteredValue;
576: }
577: contentHandler.characters(stringValue.toCharArray(), 0,
578: stringValue.length());
579: contentHandler.endElement(FormsConstants.INSTANCE_NS,
580: VALUE_EL, FormsConstants.INSTANCE_PREFIX_COLON
581: + VALUE_EL);
582: }
583:
584: // Suggested label, if any
585: String suggestedLabel = getSuggestionLabel();
586: if (suggestedLabel != null) {
587: contentHandler.startElement(FormsConstants.INSTANCE_NS,
588: "suggestion", FormsConstants.INSTANCE_PREFIX_COLON
589: + "suggestion", XMLUtils.EMPTY_ATTRIBUTES);
590: contentHandler.characters(suggestedLabel.toCharArray(), 0,
591: suggestedLabel.length());
592: contentHandler.endElement(FormsConstants.INSTANCE_NS,
593: "suggestion", FormsConstants.INSTANCE_PREFIX_COLON
594: + "suggestion");
595: }
596:
597: // validation message element: only present if the value is not valid
598: if (validationError != null
599: && (this .valueState == VALUE_DISPLAY_VALIDATION || this .valueState == VALUE_DISPLAY_PARSE_ERROR)) {
600: contentHandler.startElement(FormsConstants.INSTANCE_NS,
601: VALIDATION_MSG_EL,
602: FormsConstants.INSTANCE_PREFIX_COLON
603: + VALIDATION_MSG_EL,
604: XMLUtils.EMPTY_ATTRIBUTES);
605: validationError.generateSaxFragment(contentHandler);
606: contentHandler.endElement(FormsConstants.INSTANCE_NS,
607: VALIDATION_MSG_EL,
608: FormsConstants.INSTANCE_PREFIX_COLON
609: + VALIDATION_MSG_EL);
610: }
611:
612: // generate selection list, if any
613: if (selectionList != null) {
614: selectionList.generateSaxFragment(contentHandler, locale);
615: }
616:
617: // include some info about the datatype
618: definition.getDatatype().generateSaxFragment(contentHandler,
619: locale);
620: }
621:
622: /**
623: * Set this field's selection list.
624: * @param selectionList The new selection list.
625: */
626: public void setSelectionList(SelectionList selectionList) {
627: if (selectionList != null
628: && selectionList.getDatatype() != null
629: && selectionList.getDatatype() != getDatatype()) {
630: throw new RuntimeException(
631: "Tried to assign a SelectionList that is not associated with this widget's datatype.");
632: }
633: this .selectionList = selectionList;
634: getForm().addWidgetUpdate(this );
635: }
636:
637: /**
638: * Read this field's selection list from an external source.
639: * All Cocoon-supported protocols can be used.
640: * The format of the XML produced by the source should be the
641: * same as in case of inline specification of the selection list,
642: * thus the root element should be a <code>fd:selection-list</code>
643: * element.
644: * @param uri The URI of the source.
645: */
646: public void setSelectionList(String uri) {
647: setSelectionList(getFieldDefinition().buildSelectionList(uri));
648: }
649:
650: /**
651: * Set this field's selection list using values from an in-memory
652: * object. The <code>object</code> parameter should point to a collection
653: * (Java collection or array, or Javascript array) of objects. Each object
654: * belonging to the collection should have a <em>value</em> property and a
655: * <em>label</em> property, whose values are used to specify the <code>value</code>
656: * attribute and the contents of the <code>fd:label</code> child element
657: * of every <code>fd:item</code> in the list.
658: * <p>Access to the values of the above mentioned properties is done
659: * via <a href="http://jakarta.apache.org/commons/jxpath/users-guide.html">XPath</a> expressions.
660: * @param model The collection used as a model for the selection list.
661: * @param valuePath An XPath expression referring to the attribute used
662: * to populate the values of the list's items.
663: * @param labelPath An XPath expression referring to the attribute used
664: * to populate the labels of the list's items.
665: */
666: public void setSelectionList(Object model, String valuePath,
667: String labelPath) {
668: setSelectionList(getFieldDefinition()
669: .buildSelectionListFromModel(model, valuePath,
670: labelPath));
671: }
672:
673: public SelectionList getSuggestionList() {
674: return getFieldDefinition().getSuggestionList();
675: }
676:
677: public Datatype getDatatype() {
678: return getFieldDefinition().getDatatype();
679: }
680:
681: /**
682: * Adds a ValueChangedListener to this widget instance. Listeners defined
683: * on the widget instance will be executed in addtion to any listeners
684: * that might have been defined in the widget definition.
685: */
686: public void addValueChangedListener(ValueChangedListener listener) {
687: this .listener = WidgetEventMulticaster.add(this .listener,
688: listener);
689: }
690:
691: public void removeValueChangedListener(ValueChangedListener listener) {
692: this .listener = WidgetEventMulticaster.remove(this .listener,
693: listener);
694: }
695:
696: public boolean hasValueChangedListeners() {
697: return this .listener != null;
698: }
699:
700: public void broadcastEvent(WidgetEvent event) {
701: if (event instanceof ValueChangedEvent) {
702: if (this .listener != null) {
703: this .listener.valueChanged((ValueChangedEvent) event);
704: }
705: } else {
706: // Other kinds of events
707: super.broadcastEvent(event);
708: }
709: }
710: }
|