/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * Copyright (c) 2013, MPL CodeInside http://codeinside.ru */ package ru.codeinside.gses.vaadin.customfield; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import com.vaadin.data.Property; import com.vaadin.data.Property.ReadOnlyStatusChangeListener; import com.vaadin.data.Property.ReadOnlyStatusChangeNotifier; import com.vaadin.data.Validatable; import com.vaadin.data.Validator; import com.vaadin.data.Validator.InvalidValueException; import com.vaadin.terminal.CompositeErrorMessage; import com.vaadin.terminal.ErrorMessage; import com.vaadin.terminal.PaintException; import com.vaadin.terminal.PaintTarget; import com.vaadin.ui.AbstractComponent; import com.vaadin.ui.AbstractSelect; import com.vaadin.ui.Component; import com.vaadin.ui.ComponentContainer; import com.vaadin.ui.CustomComponent; import com.vaadin.ui.Field; public abstract class FieldWrapper<PC> extends CustomComponent implements Field, ReadOnlyStatusChangeNotifier, ReadOnlyStatusChangeListener { /** * Property converter that delegates conversions back to the containing * class instance. */ protected class DefaultPropertyConverter extends PropertyConverter<PC, Object> { public DefaultPropertyConverter(Class<? extends PC> propertyClass) { super(propertyClass); } @Override public Object format(PC value) { return FieldWrapper.this.format(value); } @Override public PC parse(Object formattedValue) throws ConversionException { return FieldWrapper.this.parse(formattedValue); } } /** * The {@link Field} to which most functionality is delegated. */ private Field wrappedField; /** * Property value converter or null if none is used. */ private PropertyConverter<PC, ? extends Object> converter; /** * The property used, either a {@link PropertyConverter} or the wrapped * field. */ private Property property; /** * Type of the data for the underlying property. */ private Class<? extends PC> propertyType; /** * The tab order number of this field. */ private int tabIndex = 0; /** * Create a custom field wrapping a {@link Field}. * * Subclass constructors calling this constructor must create and set the * layout. * * When this constructor is used, value conversions are delegated to the * methods {@link #format(PC)} and {@link #parse(Object)}. * * @param wrappedField * @param propertyType */ protected FieldWrapper(Field wrappedField, Class<? extends PC> propertyType) { this.wrappedField = wrappedField; this.propertyType = propertyType; converter = new DefaultPropertyConverter(propertyType); converter.setPropertyDataSource(wrappedField.getPropertyDataSource()); wrappedField.setPropertyDataSource(converter); property = converter; } /** * Create a custom field wrapping a {@link Field}. * * Subclass constructors calling this constructor must create and set the * layout. * * When this constructor is used, value conversions are delegated to the * methods {@link #format(PC)} and {@link #parse(Object)}. * * @param wrappedField * @param propertyType * @param layout * composition root layout, which already contains the wrapped * field */ protected FieldWrapper(Field wrappedField, Class<? extends PC> propertyType, ComponentContainer layout) { this(wrappedField, propertyType); setCompositionRoot(layout); } /** * Create a custom field wrapping a {@link Field} with a user-defined * {@link PropertyConverter}. * * Subclass constructors calling this constructor must create and set the * layout. * * When this constructor is used, the methods {@link #format(PC)} and * {@link #parse(Object)} are never called. * * @param wrappedField * @param propertyConverter * or null to bypass the use of a property converter * @param propertyType */ protected FieldWrapper(Field wrappedField, PropertyConverter<PC, ? extends Object> converter, Class<? extends PC> propertyType) { this.wrappedField = wrappedField; this.converter = converter; this.propertyType = propertyType; if (converter != null) { converter.setPropertyDataSource(wrappedField .getPropertyDataSource()); wrappedField.setPropertyDataSource(converter); property = converter; } else { property = wrappedField; } } /** * Create a custom field wrapping a {@link Field} with a user-defined * {@link PropertyConverter}. * * Subclass constructors calling this constructor must create and set the * layout. * * When this constructor is used, the methods {@link #format(PC)} and * {@link #parse(Object)} are never called. * * @param wrappedField * @param propertyConverter * or null to bypass the use of a property converter * @param propertyType * @param layout * composition root layout, which already contains the wrapped * field */ protected FieldWrapper(Field wrappedField, PropertyConverter<PC, ? extends Object> converter, Class<? extends PC> propertyType, ComponentContainer layout) { this(wrappedField, converter, propertyType); setCompositionRoot(layout); } /** * Returns the wrapped field to which operations are delegated. * * @return */ protected Field getWrappedField() { return wrappedField; } /** * Returns the property converter performing value conversions etc. * * By default, if no property converter is given, a * {@link DefaultPropertyConverter} is created, but the user can explicitly * specify null as the converter when calling the constructor. * * @return property converter or null if none */ protected PropertyConverter<PC, ? extends Object> getConverter() { return converter; } /** * Returns the property to which operations are delegated first if it * supports them. Currently, this is either the {@link PropertyConverter} * used or the wrapped {@link Field}. * * This method is for internal use only. * * @return property, not null */ protected Property getProperty() { return property; } /** * Convert an underlying property value to a field value to display. * * The default conversion uses toString(), override or specify another * converter to modify behavior. */ protected Object format(PC value) { return value != null ? value.toString() : null; } /** * Convert a field value to an underlying property value. * * The default is no conversion, override or specify another converter to * modify behavior. */ protected PC parse(Object formattedValue) throws ConversionException { return (PC) formattedValue; } public Class<? extends PC> getType() { return propertyType; } @Override public void paintContent(PaintTarget target) throws PaintException { // The tab ordering number if (tabIndex != 0) { target.addAttribute("tabindex", tabIndex); } // If the field is modified, but not committed, set modified attribute if (isModified()) { target.addAttribute("modified", true); } // Adds the required attribute if (!isReadOnly() && isRequired()) { target.addAttribute("required", true); } // Hide the error indicator if needed if (isRequired() && isEmpty() && getComponentError() == null && getErrorMessage() != null) { target.addAttribute("hideErrors", true); } super.paintContent(target); } /** * Is the field empty? * * In general, "empty" state is same as null. If the wrapped field is an * {@link AbstractSelect} in multiselect mode, also an empty * {@link Collection} is considered to be empty. * * Override if custom functionality is needed. This method should always * return "true" for null values. */ protected boolean isEmpty() { // getValue() also handles read-through mode Object value = getValue(); return (value == null) || ((wrappedField instanceof AbstractSelect) && ((AbstractSelect) wrappedField).isMultiSelect() && (value instanceof Collection) && ((Collection) value) .isEmpty()); } @Override public void focus() { super.focus(); } public boolean isInvalidCommitted() { return wrappedField.isInvalidCommitted(); } public void setInvalidCommitted(boolean isCommitted) { wrappedField.setInvalidCommitted(isCommitted); } public void commit() throws SourceException, InvalidValueException { wrappedField.commit(); } public void discard() throws SourceException { wrappedField.discard(); } public boolean isWriteThrough() { return wrappedField.isWriteThrough(); } public void setWriteThrough(boolean writeThrough) throws SourceException, InvalidValueException { wrappedField.setWriteThrough(writeThrough); } public boolean isReadThrough() { return wrappedField.isReadThrough(); } public void setReadThrough(boolean readThrough) throws SourceException { wrappedField.setReadThrough(readThrough); } public boolean isModified() { return wrappedField.isModified(); } public void addValidator(Validator validator) { if (property instanceof Validatable) { ((Validatable) property).addValidator(validator); requestRepaint(); } else { wrappedField.addValidator(validator); } } public void removeValidator(Validator validator) { if (property instanceof Validatable) { ((Validatable) property).removeValidator(validator); requestRepaint(); } else { wrappedField.removeValidator(validator); } } public Collection<Validator> getValidators() { if (property instanceof Validatable) { return ((Validatable) property).getValidators(); } else { return wrappedField.getValidators(); } } public boolean isValid() { if (property instanceof Validatable) { if (isEmpty()) { if (isRequired()) { return false; } else { return true; } } if (converter != null) { return converter.isValid(getValue()); } else { return ((Validatable) getProperty()).isValid(); } } else { return wrappedField.isValid(); } } public void validate() throws InvalidValueException { if (property instanceof Validatable) { if (isEmpty()) { if (isRequired()) { throw new Validator.EmptyValueException(getRequiredError()); } else { return; } } if (converter != null) { converter.validate(getValue()); } else { ((Validatable) property).validate(); } } else { wrappedField.validate(); } } /** * Returns the error message of the component, the wrapped field and the * validation of this field (if it has a converter or other custom * property). The error messages are combined if necessary. * * Note that the method {@link #validate()} of this component is not called * if there is no custom property/converter. This is to avoid duplicate * error messages - override this method to change the behavior if * necessary. * * Note also that {@link AbstractComponent#setComponentError()} is not * overridden, and setting the error message for this component does not * affect the error message of the wrapped field. Override the * setComponentError() method to modify this behavior. * * If overriding this method, see * {@link #getAbstractComponentErrorMessage()}, * {@link #getValidationError()} and * {@link #combineErrorMessages(ErrorMessage[])}. */ @Override public ErrorMessage getErrorMessage() { ErrorMessage superError = getAbstractComponentErrorMessage(); // this is needed to get buffered source exceptions ErrorMessage fieldError = null; if (wrappedField instanceof AbstractComponent) { fieldError = ((AbstractComponent) wrappedField).getErrorMessage(); } // should do this always, but that could lead to duplicate errors with // wrappedField ErrorMessage validationError = null; if (property instanceof Validatable && property != wrappedField) { validationError = getValidationError(); } return combineErrorMessages(new ErrorMessage[] { superError, validationError, fieldError }); } /** * Returns the error message of this component, without taking the wrapped * field into account. This is sometimes needed when overriding the behavior * of {@link #getErrorMessage()}. * * @return error message of this component, ignoring the wrapped field */ protected ErrorMessage getAbstractComponentErrorMessage() { return super.getErrorMessage(); } /** * Perform validation of the field and return the validation error found, if * any. * * @return */ protected ErrorMessage getValidationError() { try { validate(); } catch (Validator.InvalidValueException e) { if (!e.isInvisible()) { return e; } } return null; } /** * Combine multiple {@link ErrorMessage} instances into a single message, * using {@link CompositeErrorMessage} if necessary. * * Any input {@link CompositeErrorMessage} instances are flattened and null * messages filtered out, and empty input results in the return value null. * * @param errorMessages * non-null array of error messages (may contain null) * @return */ protected ErrorMessage combineErrorMessages(ErrorMessage[] errorMessages) { // combine error messages from all sources List<ErrorMessage> errors = new ArrayList<ErrorMessage>(); if (errorMessages.length == 1 && errorMessages[0] != null) { return errorMessages[0]; } for (ErrorMessage errorMessage : errorMessages) { if (errorMessage instanceof CompositeErrorMessage) { // flatten the hierarchy of composite errors Iterator<ErrorMessage> it = ((CompositeErrorMessage) errorMessage) .iterator(); while (it.hasNext()) { // never null for CompositeErrorMessage errors.add(it.next()); } } else if (errorMessage != null) { errors.add(errorMessage); } } if (errors.isEmpty()) { return null; } else if (errors.size() == 1) { return errors.get(0); } else { return new CompositeErrorMessage(errors); } } public boolean isInvalidAllowed() { if (property instanceof Validatable) { return ((Validatable) property).isInvalidAllowed(); } else { return wrappedField.isInvalidAllowed(); } } public void setInvalidAllowed(boolean invalidValueAllowed) throws UnsupportedOperationException { if (property instanceof Validatable) { ((Validatable) property).setInvalidAllowed(invalidValueAllowed); } else { wrappedField.setInvalidAllowed(invalidValueAllowed); } } public PC getValue() { if (!isReadThrough() || isModified()) { // return internal value (converted) Object internalValue = getWrappedField().getValue(); if (converter != null) { return ((PropertyConverter<PC, Object>) converter) .parse(internalValue); } else if (internalValue != null && getType().isAssignableFrom(internalValue.getClass())) { return (PC) internalValue; } else { return null; } } else { // return property value if (converter != null) { return (PC) converter.getPropertyDataSource().getValue(); } else { return (PC) property.getValue(); } } } public void setValue(Object newValue) throws ReadOnlyException, ConversionException { if (converter != null) { converter.getPropertyDataSource().setValue(newValue); } else { property.setValue(newValue); } } public void addListener(ValueChangeListener listener) { // if possible, listener for the original datasource values, not the // wrapped field converted values if (converter != null && converter.getPropertyDataSource() instanceof Property.ValueChangeNotifier) { ((Property.ValueChangeNotifier) converter.getPropertyDataSource()) .addListener(listener); } else if (property instanceof Property.ValueChangeNotifier) { ((Property.ValueChangeNotifier) property).addListener(listener); } else { wrappedField.addListener(listener); } } public void removeListener(ValueChangeListener listener) { // see addListener() if (converter != null && converter.getPropertyDataSource() instanceof Property.ValueChangeNotifier) { ((Property.ValueChangeNotifier) converter.getPropertyDataSource()) .removeListener(listener); } else if (property instanceof Property.ValueChangeNotifier) { ((Property.ValueChangeNotifier) property).removeListener(listener); } else { wrappedField.removeListener(listener); } } public void valueChange(com.vaadin.data.Property.ValueChangeEvent event) { // this should also work if property is a PropertyConverter if (property instanceof Property.ValueChangeListener) { ((Property.ValueChangeListener) property).valueChange(event); } else { wrappedField.valueChange(event); } } public void addListener(Property.ReadOnlyStatusChangeListener listener) { if (property instanceof ReadOnlyStatusChangeNotifier) { ((ReadOnlyStatusChangeNotifier) property).addListener(listener); } } public void removeListener(Property.ReadOnlyStatusChangeListener listener) { if (property instanceof ReadOnlyStatusChangeNotifier) { ((ReadOnlyStatusChangeNotifier) property).removeListener(listener); } } public void readOnlyStatusChange(Property.ReadOnlyStatusChangeEvent event) { if (property instanceof ReadOnlyStatusChangeListener) { ((ReadOnlyStatusChangeListener) property) .readOnlyStatusChange(event); } } public void setPropertyDataSource(Property newDataSource) { if (converter != null) { // note that assuming property == converter in this case converter.setPropertyDataSource(newDataSource); } else { wrappedField.setPropertyDataSource(newDataSource); } } /** * Returns the property data source for the field. * * Note that this method for {@link FieldWrapper} always returns the * property converter, or the property data source of the wrapped field if * there is no converter. */ public Property getPropertyDataSource() { if (converter != null) { return converter; } else { return wrappedField.getPropertyDataSource(); } } public int getTabIndex() { return tabIndex; } public void setTabIndex(int tabIndex) { this.tabIndex = tabIndex; } public boolean isRequired() { if (property instanceof Field) { return ((Field) property).isRequired(); } else { return wrappedField.isRequired(); } } public void setRequired(boolean required) { if (property instanceof Field) { ((Field) property).setRequired(required); } else { wrappedField.setRequired(required); } } public void setRequiredError(String requiredMessage) { if (property instanceof Field) { ((Field) property).setRequiredError(requiredMessage); } else { wrappedField.setRequiredError(requiredMessage); } } public String getRequiredError() { if (property instanceof Field) { return ((Field) property).getRequiredError(); } else { return wrappedField.getRequiredError(); } } @Override public boolean isReadOnly() { return super.isReadOnly() || property.isReadOnly(); } @Override public void setReadOnly(boolean readOnly) { property.setReadOnly(readOnly); super.setReadOnly(readOnly); } @Override public void setImmediate(boolean immediate) { super.setImmediate(immediate); if (wrappedField instanceof AbstractComponent) { ((AbstractComponent) wrappedField).setImmediate(immediate); } } }