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.woody.formmodel;
018:
019: import org.apache.cocoon.woody.Constants;
020: import org.apache.cocoon.woody.FormContext;
021: import org.apache.cocoon.woody.util.I18nMessage;
022: import org.apache.cocoon.woody.validation.ValidationError;
023: import org.apache.cocoon.woody.validation.ValidationErrorAware;
024: import org.apache.cocoon.woody.datatype.SelectionList;
025: import org.apache.cocoon.woody.datatype.Datatype;
026: import org.apache.cocoon.woody.event.DeferredValueChangedEvent;
027: import org.apache.cocoon.woody.event.WidgetEvent;
028: import org.apache.cocoon.woody.event.ValueChangedEvent;
029: import org.apache.cocoon.xml.AttributesImpl;
030: import org.xml.sax.ContentHandler;
031: import org.xml.sax.SAXException;
032:
033: import java.util.Locale;
034:
035: /**
036: * A general-purpose Widget that can hold one value. A Field widget can be associated
037: * with a {@link org.apache.cocoon.woody.datatype.Datatype Datatype}, and thus
038: * a Field widget can be used to edit different kinds of data, such as strings,
039: * numbers and dates. A Datatype can also have an associated SelectionList, so
040: * that the value for the Field can be selected from a list, rather than being
041: * entered in a textbox. The validation of the field is delegated to its associated
042: * Datatype.
043: *
044: * @author Bruno Dumon
045: * @author <a href="http://www.apache.org/~sylvain/">Sylvain Wallez</a>
046: * @version CVS $Id: Field.java 433543 2006-08-22 06:22:54Z crossley $
047: */
048: public class Field extends AbstractWidget implements
049: ValidationErrorAware, DataWidget, SelectableWidget {
050: protected SelectionList selectionList;
051:
052: protected String enteredValue;
053: protected Object value;
054:
055: // At startup, we don't need to parse (both enteredValue and value are null),
056: // but need to validate (error if field is required)
057: protected boolean needsParse = true;
058: protected boolean needsValidate = true;
059: private boolean isValidating;
060: protected ValidationError validationError;
061:
062: public Field(FieldDefinition fieldDefinition) {
063: setDefinition(fieldDefinition);
064: setLocation(fieldDefinition.getLocation());
065: }
066:
067: public final FieldDefinition getFieldDefinition() {
068: return (FieldDefinition) super .definition;
069: }
070:
071: public String getId() {
072: return definition.getId();
073: }
074:
075: public Object getValue() {
076: // Parse the value
077: if (this .needsParse) {
078: // Clear value, it will be recomputed
079: this .value = null;
080: if (this .enteredValue != null) {
081: // Parse the value
082: this .value = getDatatype().convertFromString(
083: this .enteredValue, getForm().getLocale());
084: if (this .value != null) { // Conversion successfull
085: this .needsParse = false;
086: this .needsValidate = true;
087: } else { // Conversion failed
088: this .validationError = new ValidationError(
089: new I18nMessage(
090: "datatype.conversion-failed",
091: new String[] { "datatype."
092: + getDatatype()
093: .getDescriptiveName() },
094: new boolean[] { true },
095: Constants.I18N_CATALOGUE));
096: // No need for further validation (and need to keep the above error)
097: this .needsValidate = false;
098: }
099: } else {
100: this .needsParse = false;
101: this .needsValidate = true;
102: }
103: }
104:
105: // if getValue() is called on this field while we're validating, then it's because a validation
106: // rule called getValue(), so then we just return the parsed (but not validated) value to avoid an endless loop
107: if (isValidating) {
108: return value;
109: }
110:
111: // Validate the value
112: if (this .needsValidate) {
113: isValidating = true;
114: try {
115: if (super .validate(null)) {
116: // New-style validators were successful. Check the old-style ones.
117: if (this .value != null) {
118: this .validationError = getDatatype().validate(
119: value, new ExpressionContextImpl(this ));
120: } else { // No value : is it required ?
121: if (getFieldDefinition().isRequired()) {
122: this .validationError = new ValidationError(
123: new I18nMessage(
124: "general.field-required",
125: Constants.I18N_CATALOGUE));
126: }
127: }
128: }
129: this .needsValidate = false;
130: } finally {
131: isValidating = false;
132: }
133: }
134: return this .validationError == null ? this .value : null;
135: }
136:
137: public void setValue(Object newValue) {
138: if (newValue != null
139: && !getDatatype().getTypeClass().isAssignableFrom(
140: newValue.getClass())) {
141: throw new RuntimeException("Incorrect value type for \""
142: + getFullyQualifiedId() + "\" (expected "
143: + getDatatype().getTypeClass() + ", got "
144: + newValue.getClass() + ".");
145: }
146: Object oldValue = this .value;
147: boolean changed = !(oldValue == null ? "" : oldValue)
148: .equals(newValue == null ? "" : newValue);
149: // Do something only if value is different or null
150: // (null allows to reset validation error)
151: if (changed || newValue == null) {
152: this .value = newValue;
153: this .needsParse = false;
154: this .validationError = null;
155: // Force validation, even if set by the application
156: this .needsValidate = true;
157: if (newValue != null) {
158: this .enteredValue = getDatatype().convertToString(
159: newValue, getForm().getLocale());
160: } else {
161: this .enteredValue = null;
162: }
163: if (changed) {
164: getForm()
165: .addWidgetEvent(
166: new ValueChangedEvent(this , oldValue,
167: newValue));
168: }
169: }
170: }
171:
172: public void readFromRequest(FormContext formContext) {
173: String newEnteredValue = formContext.getRequest().getParameter(
174: getFullyQualifiedId());
175: readFromRequest(newEnteredValue);
176: }
177:
178: protected void readFromRequest(String newEnteredValue) {
179: // whitespace & empty field handling
180: if (newEnteredValue != null) {
181: // TODO make whitespace behaviour configurable !!
182: newEnteredValue = newEnteredValue.trim();
183: if (newEnteredValue.length() == 0) {
184: newEnteredValue = null;
185: }
186: }
187:
188: // Only convert if the text value actually changed. Otherwise, keep the old value
189: // and/or the old validation error (allows to keep errors when clicking on actions)
190: if (!(newEnteredValue == null ? "" : newEnteredValue)
191: .equals((enteredValue == null ? "" : enteredValue))) {
192: getForm().addWidgetEvent(
193: new DeferredValueChangedEvent(this , value));
194: enteredValue = newEnteredValue;
195: validationError = null;
196: value = null;
197: needsParse = true;
198: }
199:
200: // Always revalidate, as validation may depend on the value of other fields
201: this .needsValidate = true;
202: }
203:
204: public boolean validate(FormContext formContext) {
205: // If needed, getValue() will do the validation
206: getValue();
207: return this .validationError == null;
208: }
209:
210: /**
211: * Returns the validation error, if any. There will always be a validation error in case the
212: * {@link #validate(FormContext)} method returned false.
213: */
214: public ValidationError getValidationError() {
215: return validationError;
216: }
217:
218: /**
219: * Set a validation error on this field. This allows fields to be externally marked as invalid by
220: * application logic.
221: *
222: * @param error the validation error
223: */
224: public void setValidationError(ValidationError error) {
225: this .validationError = error;
226: }
227:
228: public boolean isRequired() {
229: return getFieldDefinition().isRequired();
230: }
231:
232: private static final String FIELD_EL = "field";
233: private static final String VALUE_EL = "value";
234: private static final String VALIDATION_MSG_EL = "validation-message";
235:
236: public void generateSaxFragment(ContentHandler contentHandler,
237: Locale locale) throws SAXException {
238: AttributesImpl fieldAttrs = new AttributesImpl();
239: fieldAttrs.addCDATAAttribute("id", getFullyQualifiedId());
240: fieldAttrs.addCDATAAttribute("required", String
241: .valueOf(isRequired()));
242: contentHandler.startElement(Constants.WI_NS, FIELD_EL,
243: Constants.WI_PREFIX_COLON + FIELD_EL, fieldAttrs);
244:
245: if (enteredValue != null || value != null) {
246: contentHandler.startElement(Constants.WI_NS, VALUE_EL,
247: Constants.WI_PREFIX_COLON + VALUE_EL,
248: Constants.EMPTY_ATTRS);
249: String stringValue;
250: if (value != null) {
251: stringValue = getDatatype().convertToString(value,
252: locale);
253: } else {
254: stringValue = enteredValue;
255: }
256: contentHandler.characters(stringValue.toCharArray(), 0,
257: stringValue.length());
258: contentHandler.endElement(Constants.WI_NS, VALUE_EL,
259: Constants.WI_PREFIX_COLON + VALUE_EL);
260: }
261:
262: // validation message element: only present if the value is not valid
263: if (validationError != null) {
264: contentHandler.startElement(Constants.WI_NS,
265: VALIDATION_MSG_EL, Constants.WI_PREFIX_COLON
266: + VALIDATION_MSG_EL, Constants.EMPTY_ATTRS);
267: validationError.generateSaxFragment(contentHandler);
268: contentHandler.endElement(Constants.WI_NS,
269: VALIDATION_MSG_EL, Constants.WI_PREFIX_COLON
270: + VALIDATION_MSG_EL);
271: }
272:
273: // generate label, help, hint, etc.
274: definition.generateDisplayData(contentHandler);
275:
276: // generate selection list, if any
277: if (selectionList != null) {
278: selectionList.generateSaxFragment(contentHandler, locale);
279: } else if (getFieldDefinition().getSelectionList() != null) {
280: getFieldDefinition().getSelectionList()
281: .generateSaxFragment(contentHandler, locale);
282: }
283: contentHandler.endElement(Constants.WI_NS, FIELD_EL,
284: Constants.WI_PREFIX_COLON + FIELD_EL);
285: }
286:
287: public void generateLabel(ContentHandler contentHandler)
288: throws SAXException {
289: definition.generateLabel(contentHandler);
290: }
291:
292: /**
293: * Set this field's selection list.
294: * @param selectionList The new selection list.
295: */
296: public void setSelectionList(SelectionList selectionList) {
297: if (selectionList != null
298: && selectionList.getDatatype() != null
299: && selectionList.getDatatype() != getDatatype()) {
300: throw new RuntimeException(
301: "Tried to assign a SelectionList that is not associated with this widget's datatype.");
302: }
303: this .selectionList = selectionList;
304: }
305:
306: /**
307: * Read this field's selection list from an external source.
308: * All Cocoon-supported protocols can be used.
309: * The format of the XML produced by the source should be the
310: * same as in case of inline specification of the selection list,
311: * thus the root element should be a <code>wd:selection-list</code>
312: * element.
313: * @param uri The URI of the source.
314: */
315: public void setSelectionList(String uri) {
316: setSelectionList(getFieldDefinition().buildSelectionList(uri));
317: }
318:
319: /**
320: * Set this field's selection list using values from an in-memory
321: * object. The <code>object</code> parameter should point to a collection
322: * (Java collection or array, or Javascript array) of objects. Each object
323: * belonging to the collection should have a <em>value</em> property and a
324: * <em>label</em> property, whose values are used to specify the <code>value</code>
325: * attribute and the contents of the <code>wd:label</code> child element
326: * of every <code>wd:item</code> in the list.
327: * <p>Access to the values of the above mentioned properties is done
328: * via <a href="http://jakarta.apache.org/commons/jxpath/users-guide.html">XPath</a> expressions.
329: * @param model The collection used as a model for the selection list.
330: * @param valuePath An XPath expression referring to the attribute used
331: * to populate the values of the list's items.
332: * @param labelPath An XPath expression referring to the attribute used
333: * to populate the labels of the list's items.
334: */
335: public void setSelectionList(Object model, String valuePath,
336: String labelPath) {
337: setSelectionList(getFieldDefinition()
338: .buildSelectionListFromModel(model, valuePath,
339: labelPath));
340: }
341:
342: public Datatype getDatatype() {
343: return getFieldDefinition().getDatatype();
344: }
345:
346: public void broadcastEvent(WidgetEvent event) {
347: getFieldDefinition().fireValueChangedEvent(
348: (ValueChangedEvent) event);
349: }
350: }
|