/* ************************************************************************* IT Mill Toolkit Development of Browser User Interfaces Made Easy Copyright (C) 2000-2006 IT Mill Ltd ************************************************************************* This product is distributed under commercial license that can be found from the product package on license.pdf. Use of this product might require purchasing a commercial license from IT Mill Ltd. For guidelines on usage, see licensing-guidelines.html ************************************************************************* For more information, contact: IT Mill Ltd phone: +358 2 4802 7180 Ruukinkatu 2-4 fax: +358 2 4802 7181 20540, Turku email: info@itmill.com Finland company www: www.itmill.com Primary source for information and releases: www.itmill.com ********************************************************************** */ package com.itmill.toolkit.ui; import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.LinkedList; import com.itmill.toolkit.data.Buffered; import com.itmill.toolkit.data.Property; import com.itmill.toolkit.data.Validatable; import com.itmill.toolkit.data.Validator; import com.itmill.toolkit.terminal.ErrorMessage; import com.itmill.toolkit.terminal.PaintException; import com.itmill.toolkit.terminal.PaintTarget; /** *

* Abstract field component for implementing buffered property editors. The * field may hold an internal value, or it may be connected to any data source * that implements the {@link com.itmill.toolkit.data.Property}interface. * AbstractField implements that interface itself, too, so * accessing the Property value represented by it is straightforward. *

* *

* AbstractField also provides the {@link com.itmill.toolkit.data.Buffered} * interface for buffering the data source value. By default the Field is in * write through-mode and {@link #setWriteThrough(boolean)}should be called to * enable buffering. *

* *

* The class also supports {@link com.itmill.toolkit.data.Validator validators} * to make sure the value contained in the field is valid. *

* * @author IT Mill Ltd. * @version * @VERSION@ * @since 3.0 */ public abstract class AbstractField extends AbstractComponent implements Field, Property.ReadOnlyStatusChangeNotifier { /* Private members ************************************************* */ private boolean delayedFocus; /** * Value of the datafield. */ private Object value; /** * Connected data-source. */ private Property dataSource = null; /** * The list of validators. */ private LinkedList validators = null; /** * Auto commit mode. */ private boolean writeTroughMode = true; /** * Reads the value from data-source, when it is not modified. */ private boolean readTroughMode = true; /** * Is the field modified but not committed. */ private boolean modified = false; /** * Current source exception. */ private Buffered.SourceException currentBufferedSourceException = null; /** * Are the invalid values alloved in fields ? */ private boolean invalidAllowed = true; /** * Are the invalid values committed ? */ private boolean invalidCommitted = false; /** * The tab order number of this field. */ private int tabIndex = 0; /** * Unique focusable id. */ private long focusableId = -1; /** * Required field. */ private boolean required = false; /* Component basics ************************************************ */ public AbstractField() { this.focusableId = Window.getNewFocusableId(this); } /* * Paints the field. Don't add a JavaDoc comment here, we use the default * documentation from the implemented interface. */ public void paintContent(PaintTarget target) throws PaintException { // Focus control id if (this.focusableId > 0) { target.addAttribute("focusid", this.focusableId); } // The tab ordering number if (this.tabIndex > 0) target.addAttribute("tabindex", this.tabIndex); // If the field is modified, but not committed, set modified attribute if (isModified()) target.addAttribute("modified", true); // Adds the required attribute if (isRequired()) target.addAttribute("required", true); } /* * Gets the field type Don't add a JavaDoc comment here, we use the default * documentation from the implemented interface. */ public abstract Class getType(); /** * The abstract field is read only also if the data source is in readonly * mode. */ public boolean isReadOnly() { return super.isReadOnly() || (dataSource != null && dataSource.isReadOnly()); } /** * Changes the readonly state and throw read-only status change events. * * @see com.itmill.toolkit.ui.Component#setReadOnly(boolean) */ public void setReadOnly(boolean readOnly) { super.setReadOnly(readOnly); fireReadOnlyStatusChange(); } /** * Tests if the invalid data is committed to datasource. * * @see com.itmill.toolkit.data.BufferedValidatable#isInvalidCommitted() */ public boolean isInvalidCommitted() { return invalidCommitted; } /** * Sets if the invalid data should be committed to datasource. * * @see com.itmill.toolkit.data.BufferedValidatable#setInvalidCommitted(boolean) */ public void setInvalidCommitted(boolean isCommitted) { this.invalidCommitted = isCommitted; } /* * Saves the current value to the data source Don't add a JavaDoc comment * here, we use the default documentation from the implemented interface. */ public void commit() throws Buffered.SourceException { if (dataSource != null && (isInvalidCommitted() || isValid()) && !dataSource.isReadOnly()) { Object newValue = getValue(); try { // Commits the value to datasource. dataSource.setValue(newValue); } catch (Throwable e) { // Sets the buffering state. currentBufferedSourceException = new Buffered.SourceException( this, e); requestRepaint(); // Throws the source exception. throw currentBufferedSourceException; } } boolean repaintNeeded = false; // The abstract field is not modified anymore if (modified) { modified = false; repaintNeeded = true; } // If successful, remove set the buffering state to be ok if (currentBufferedSourceException != null) { currentBufferedSourceException = null; repaintNeeded = true; } if (repaintNeeded) requestRepaint(); } /* * Updates the value from the data source. Don't add a JavaDoc comment here, * we use the default documentation from the implemented interface. */ public void discard() throws Buffered.SourceException { if (dataSource != null) { // Gets the correct value from datasource Object newValue; try { // Discards buffer by overwriting from datasource newValue = dataSource.getValue(); // If successful, remove set the buffering state to be ok if (currentBufferedSourceException != null) { currentBufferedSourceException = null; requestRepaint(); } } catch (Throwable e) { // Sets the buffering state currentBufferedSourceException = new Buffered.SourceException( this, e); requestRepaint(); // Throws the source exception throw currentBufferedSourceException; } boolean wasModified = isModified(); modified = false; // If the new value differs from the previous one if ((newValue == null && value != null) || (newValue != null && !newValue.equals(value))) { setInternalValue(newValue); fireValueChange(false); } // If the value did not change, but the modification status did else if (wasModified) { requestRepaint(); } } } /* * Has the field been modified since the last commit()? Don't add a JavaDoc * comment here, we use the default documentation from the implemented * interface. */ public boolean isModified() { return modified; } /* * Tests if the field is in write-through mode. Don't add a JavaDoc comment * here, we use the default documentation from the implemented interface. */ public boolean isWriteThrough() { return writeTroughMode; } /* * Sets the field's write-through mode to the specified status Don't add a * JavaDoc comment here, we use the default documentation from the * implemented interface. */ public void setWriteThrough(boolean writeTrough) throws Buffered.SourceException { if (writeTroughMode == writeTrough) return; writeTroughMode = writeTrough; if (writeTroughMode) commit(); } /* * Tests if the field is in read-through mode. Don't add a JavaDoc comment * here, we use the default documentation from the implemented interface. */ public boolean isReadThrough() { return readTroughMode; } /* * Sets the field's read-through mode to the specified status Don't add a * JavaDoc comment here, we use the default documentation from the * implemented interface. */ public void setReadThrough(boolean readTrough) throws Buffered.SourceException { if (readTroughMode == readTrough) return; readTroughMode = readTrough; if (!isModified() && readTroughMode && dataSource != null) { setInternalValue(dataSource.getValue()); fireValueChange(false); } } /* Property interface implementation ******************************* */ /** * Returns the value of the Property in human readable textual format. * * @see java.lang.Object#toString() */ public String toString() { Object value = getValue(); if (value == null) return null; return getValue().toString(); } /** * Gets the current value of the field. This is the visible, modified and * possible invalid value the user have entered to the field. In the * read-through mode, the abstract buffer is also updated and validation is * performed. * * @return the current value of the field. */ public Object getValue() { // Give the value from abstract buffers if the field if possible if (dataSource == null || !isReadThrough() || isModified()) return value; Object newValue = dataSource.getValue(); if ((newValue == null && value != null) || (newValue != null && !newValue.equals(value))) { setInternalValue(newValue); fireValueChange(false); } return newValue; } /** * Sets the value of the field. * * @param newValue * the New value of the field. * @throws Property.ReadOnlyException * @throws Property.ConversionException */ public void setValue(Object newValue) throws Property.ReadOnlyException, Property.ConversionException { setValue(newValue, false); } /** * Sets the value of the field. * * @param newValue * the New value of the field. * @param repaintIsNotNeeded True iff caller is sure that repaint is not needed. * @throws Property.ReadOnlyException * @throws Property.ConversionException */ protected void setValue(Object newValue, boolean repaintIsNotNeeded) throws Property.ReadOnlyException, Property.ConversionException { if ((newValue == null && value != null) || (newValue != null && !newValue.equals(value))) { // Read only fields can not be changed if (isReadOnly()) throw new Property.ReadOnlyException(); // If invalid values are not allowed, the value must be checked if (!isInvalidAllowed()) { Collection v = getValidators(); if (v != null) for (Iterator i = v.iterator(); i.hasNext();) ((Validator) i.next()).validate(newValue); } // Changes the value setInternalValue(newValue); modified = dataSource != null; // In write trough mode , try to commit if (isWriteThrough() && dataSource != null && (isInvalidCommitted() || isValid())) { try { // Commits the value to datasource dataSource.setValue(newValue); // The buffer is now unmodified modified = false; } catch (Throwable e) { // Sets the buffering state currentBufferedSourceException = new Buffered.SourceException( this, e); requestRepaint(); // Throws the source exception throw currentBufferedSourceException; } } // If successful, remove set the buffering state to be ok if (currentBufferedSourceException != null) { currentBufferedSourceException = null; requestRepaint(); } // Fires the value change fireValueChange(repaintIsNotNeeded); } } /* External data source ******************************************** */ /** * Gets the current data source of the field, if any. * * @return the current data source as a Property, or null if * none defined. */ public Property getPropertyDataSource() { return dataSource; } /** *

* Sets the specified Property as the data source for the field. All * uncommitted changes to the field are discarded and the value is refreshed * from the new data source. *

* *

* If the datasource has any validators, the same validators are added to * the field. Because the default behavior of the field is to allow invalid * values, but not to allow committing them, this only adds visual error * messages to fields and do not allow committing them as long as the value * is invalid. After the value is valid, the error message is not shown and * the commit can be done normally. *

* * @param newDataSource * the new data source Property. */ public void setPropertyDataSource(Property newDataSource) { // Saves the old value Object oldValue = value; // Discards all changes to old datasource try { discard(); } catch (Buffered.SourceException ignored) { } // Stops listening the old data source changes if (dataSource != null && Property.ValueChangeNotifier.class .isAssignableFrom(dataSource.getClass())) ((Property.ValueChangeNotifier) dataSource).removeListener(this); // Sets the new data source dataSource = newDataSource; // Gets the value from source try { if (dataSource != null) setInternalValue(dataSource.getValue()); modified = false; } catch (Throwable e) { currentBufferedSourceException = new Buffered.SourceException(this, e); modified = true; } // Listens the new data source if possible if (dataSource instanceof Property.ValueChangeNotifier) ((Property.ValueChangeNotifier) dataSource).addListener(this); // Copy the validators from the data source if (dataSource instanceof Validatable) { Collection validators = ((Validatable) dataSource).getValidators(); if (validators != null) for (Iterator i = validators.iterator(); i.hasNext();) addValidator((Validator) i.next()); } // Fires value change if the value has changed if ((value != oldValue) && ((value != null && !value.equals(oldValue)) || value == null)) fireValueChange(false); } /* Validation ****************************************************** */ /** * Adds a new validator for the field's value. All validators added to a * field are checked each time the its value changes. * * @param validator * the new validator to be added. */ public void addValidator(Validator validator) { if (validators == null) validators = new LinkedList(); validators.add(validator); } /** * Gets the validators of the field. * * @return the Unmodifiable collection that holds all validators for the * field. */ public Collection getValidators() { if (validators == null || validators.isEmpty()) return null; return Collections.unmodifiableCollection(validators); } /** * Removes the validator from the field. * * @param validator * the validator to remove. */ public void removeValidator(Validator validator) { if (validators != null) validators.remove(validator); } /** * Tests the current value against all registered validators. * * @return true if all registered validators claim that the * current value is valid, false otherwise. */ public boolean isValid() { if (validators == null) return true; Object value = getValue(); for (Iterator i = validators.iterator(); i.hasNext();) if (!((Validator) i.next()).isValid(value)) return false; return true; } /** * Checks the validity of the validatable * * @see com.itmill.toolkit.data.Validatable#validate() */ public void validate() throws Validator.InvalidValueException { // If there is no validator, there can not be any errors if (validators == null) return; // Initialize temps Validator.InvalidValueException firstError = null; LinkedList errors = null; Object value = getValue(); // Gets all the validation errors for (Iterator i = validators.iterator(); i.hasNext();) try { ((Validator) i.next()).validate(value); } catch (Validator.InvalidValueException e) { if (firstError == null) firstError = e; else { if (errors == null) { errors = new LinkedList(); errors.add(firstError); } errors.add(e); } } // If there were no error if (firstError == null) return; // If only one error occurred, throw it forwards if (errors == null) throw firstError; // Creates composite validator Validator.InvalidValueException[] exceptions = new Validator.InvalidValueException[errors .size()]; int index = 0; for (Iterator i = errors.iterator(); i.hasNext();) exceptions[index++] = (Validator.InvalidValueException) i.next(); throw new Validator.InvalidValueException(null, exceptions); } /** * Fields allow invalid values by default. In most cases this is wanted, * because the field otherwise visually forget the user input immediately. * * @return true iff the invalid values are allowed. * @see com.itmill.toolkit.data.Validatable#isInvalidAllowed() */ public boolean isInvalidAllowed() { return invalidAllowed; } /** * Fields allow invalid values by default. In most cases this is wanted, * because the field otherwise visually forget the user input immediately. *

* In common setting where the user wants to assure the correctness of the * datasource, but allow temporarily invalid contents in the field, the user * should add the validators to datasource, that should not allow invalid * values. The validators are automatically copied to the field when the * datasource is set. *

* * @see com.itmill.toolkit.data.Validatable#setInvalidAllowed(boolean) */ public void setInvalidAllowed(boolean invalidAllowed) throws UnsupportedOperationException { this.invalidAllowed = invalidAllowed; } /** * Error messages shown by the fields are composites of the error message * thrown by the superclasses (that is the component error message), * validation errors and buffered source errors. * * @see com.itmill.toolkit.ui.AbstractComponent#getErrorMessage() */ public ErrorMessage getErrorMessage() { ErrorMessage superError = super.getErrorMessage(); return superError; /* * TODO: Check the logic of this ErrorMessage validationError = null; * try { validate(); } catch (Validator.InvalidValueException e) { * validationError = e; } * * if (superError == null && validationError == null && * currentBufferedSourceException == null) return null; // Throw * combination of the error types return new CompositeErrorMessage( new * ErrorMessage[] { superError, validationError, * currentBufferedSourceException }); */ } /* Value change events ****************************************** */ private static final Method VALUE_CHANGE_METHOD; static { try { VALUE_CHANGE_METHOD = Property.ValueChangeListener.class .getDeclaredMethod("valueChange", new Class[] { Property.ValueChangeEvent.class }); } catch (java.lang.NoSuchMethodException e) { // This should never happen throw new java.lang.RuntimeException(); } } /* * Adds a value change listener for the field. Don't add a JavaDoc comment * here, we use the default documentation from the implemented interface. */ public void addListener(Property.ValueChangeListener listener) { addListener(AbstractField.ValueChangeEvent.class, listener, VALUE_CHANGE_METHOD); } /* * Removes a value change listener from the field. Don't add a JavaDoc * comment here, we use the default documentation from the implemented * interface. */ public void removeListener(Property.ValueChangeListener listener) { removeListener(AbstractField.ValueChangeEvent.class, listener, VALUE_CHANGE_METHOD); } /** * Emits the value change event. The value contained in the field is * validated before the event is created. */ protected void fireValueChange(boolean repaintIsNotNeeded) { fireEvent(new AbstractField.ValueChangeEvent(this)); if (!repaintIsNotNeeded) requestRepaint(); } /* Read-only status change events *************************************** */ private static final Method READ_ONLY_STATUS_CHANGE_METHOD; static { try { READ_ONLY_STATUS_CHANGE_METHOD = Property.ReadOnlyStatusChangeListener.class .getDeclaredMethod( "readOnlyStatusChange", new Class[] { Property.ReadOnlyStatusChangeEvent.class }); } catch (java.lang.NoSuchMethodException e) { // This should never happen throw new java.lang.RuntimeException(); } } /** * An Event object specifying the Property whose read-only * status has changed. * * @author IT Mill Ltd. * @version * @VERSION@ * @since 3.0 */ public class ReadOnlyStatusChangeEvent extends Component.Event implements Property.ReadOnlyStatusChangeEvent { /** * Serial generated by eclipse. */ private static final long serialVersionUID = 3258688823264161846L; /** * New instance of text change event. * * @param source * the Source of the event. */ public ReadOnlyStatusChangeEvent(AbstractField source) { super(source); } /** * Property where the event occurred. * * @return the Source of the event. */ public Property getProperty() { return (Property) getSource(); } } /* * Adds a read-only status change listener for the field. Don't add a * JavaDoc comment here, we use the default documentation from the * implemented interface. */ public void addListener(Property.ReadOnlyStatusChangeListener listener) { addListener(Property.ReadOnlyStatusChangeEvent.class, listener, READ_ONLY_STATUS_CHANGE_METHOD); } /* * Removes a read-only status change listener from the field. Don't add a * JavaDoc comment here, we use the default documentation from the * implemented interface. */ public void removeListener(Property.ReadOnlyStatusChangeListener listener) { removeListener(Property.ReadOnlyStatusChangeEvent.class, listener, READ_ONLY_STATUS_CHANGE_METHOD); } /** * Emits the read-only status change event. The value contained in the field * is validated before the event is created. */ protected void fireReadOnlyStatusChange() { fireEvent(new AbstractField.ReadOnlyStatusChangeEvent(this)); } /** * This method listens to data source value changes and passes the changes * forwards. * * @param event * the value change event telling the data source contents have * changed. */ public void valueChange(Property.ValueChangeEvent event) { if (isReadThrough() || !isModified()) fireValueChange(false); } /** * Asks the terminal to place the cursor to this field. */ public void focus() { Window w = getWindow(); if (w != null) { w.setFocusedComponent(this); } else { this.delayedFocus = true; } } /** * Creates abstract field by the type of the property. * *

* This returns most suitable field type for editing property of given type. *

* * @param propertyType * the Type of the property, that needs to be edited. */ public static AbstractField constructField(Class propertyType) { // Null typed properties can not be edited if (propertyType == null) return null; // Date field if (Date.class.isAssignableFrom(propertyType)) { return new DateField(); } // Boolean field if (Boolean.class.isAssignableFrom(propertyType)) { Button button = new Button(""); button.setSwitchMode(true); button.setImmediate(false); return button; } // Text field is used by default return new TextField(); } /** * Gets the tab index of this field. The tab index property is used to * specify the natural tab ordering of fields. * * @return the Tab index of this field. Negative value means unspecified. */ public int getTabIndex() { return tabIndex; } /** * Gets the tab index of this field. The tab index property is used to * specify the natural tab ordering of fields. * * @param tabIndex * the tab order of this component. Negative value means * unspecified. */ public void setTabIndex(int tabIndex) { this.tabIndex = tabIndex; } /** * Sets the internal field value. This is purely used by AbstractField to * change the internal Field value. It does not trigger any events. It can * be overriden by the inheriting classes to update all dependent variables. * * @param newValue * the new value to be set. */ protected void setInternalValue(Object newValue) { this.value = newValue; } /** * Gets the unique ID of focusable * * @see com.itmill.toolkit.ui.Component.Focusable#getFocusableId() */ public long getFocusableId() { return this.focusableId; } /** * Notifies the component that it is connected to an application. * * @see com.itmill.toolkit.ui.Component#attach() */ public void attach() { super.attach(); if (this.delayedFocus) { this.delayedFocus = false; this.focus(); } } /** * Is this field required. Required fields must filled by the user. * * @return true if the field is required .otherwise * false. */ public boolean isRequired() { return required; } /** * Sets the field required. Required fields must filled by the user. * * @param required * Is the field required. */ public void setRequired(boolean required) { this.required = required; } /** * Free used resources. * * @see java.lang.Object#finalize() */ public void finalize() throws Throwable { if (focusableId > -1) { Window.removeFocusableId(focusableId); } super.finalize(); } }