/*
* 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 com.vaadin.data.*;
import com.vaadin.data.Validator.InvalidValueException;
import java.util.*;
@SuppressWarnings("serial")
public abstract class PropertyConverter<PC, FC> implements Property,
Property.ValueChangeNotifier, Property.ValueChangeListener,
Property.ReadOnlyStatusChangeListener,
Property.ReadOnlyStatusChangeNotifier, Property.Viewer, Validatable {
/** The list of validators. Def null */
private List<Validator> validators;
/** Are invalid values allowed in fields ? */
private boolean invalidAllowed = true;
/** Internal list of registered value change listeners. */
private final LinkedList<ValueChangeListener> valueChangeListeners
= new LinkedList<ValueChangeListener>();
/** Internal list of registered read-only status change listeners. */
private final LinkedList<Property.ReadOnlyStatusChangeListener> readOnlyStatusChangeListeners
= new LinkedList<ReadOnlyStatusChangeListener>();
/** Datasource that stores the actual value. */
private Property dataSource;
private Class<? extends PC> propertyClass;
/**
* Construct a new {@code PropertyConverter} that is not connected to any
* data source. Call {@link #setPropertyDataSource(Property)} later on to
* attach it to a property.
*/
protected PropertyConverter(Class<? extends PC> propertyClass) {
this.propertyClass = propertyClass;
}//new
/**
* Construct a new formatter that is connected to given data source. Calls
* {@link #format(Object)} which can be a problem if the formatter has not
* yet been initialized.
*
* @param propertyDataSource
* to connect this property to.
*/
public PropertyConverter(Property propertyDataSource) {
setPropertyDataSource(propertyDataSource);
propertyClass = (Class<? extends PC>) propertyDataSource.getType();
}//new
/**
* Gets the current data source of the formatter, if any.
*
* @return the current data source as a Property, or <code>null</code> if
* none defined.
*/
public Property getPropertyDataSource() {
return dataSource;
}
/**
* Sets the specified Property as the data source for the formatter.
*
* <p>
* Remember that new data sources getValue() must return objects that are
* compatible with parse() and format() methods.
* </p>
*
* @param newDataSource
* the new data source Property.
*/
public void setPropertyDataSource(Property newDataSource) {
boolean readOnly = false;
String prevValue = null;
if (dataSource != null) {
if (dataSource instanceof Property.ValueChangeNotifier) {
((Property.ValueChangeNotifier) dataSource)
.removeListener(this);
}
if (dataSource instanceof Property.ReadOnlyStatusChangeNotifier) {
((Property.ReadOnlyStatusChangeNotifier) dataSource)
.removeListener(this);
}
readOnly = isReadOnly();
prevValue = toString();
}
dataSource = newDataSource;
if (dataSource != null) {
if (dataSource instanceof Property.ValueChangeNotifier) {
((Property.ValueChangeNotifier) dataSource).addListener(this);
}
if (dataSource instanceof Property.ReadOnlyStatusChangeNotifier) {
((Property.ReadOnlyStatusChangeNotifier) dataSource)
.addListener(this);
}
}
if (isReadOnly() != readOnly) {
fireReadOnlyStatusChange();
}
String newVal = toString();
if ((prevValue == null && newVal != null)
|| (prevValue != null && !prevValue.equals(newVal))) {
fireValueChange();
}
}//setPropertyDataSource
public Class<? extends PC> getType() {
return propertyClass;
}
/**
* Get the formatted value.
*
* @return If the datasource returns null, this is null. Otherwise this is
* given by format().
*/
public Object getValue() {
if (dataSource == null) {
return null;
}
return format((PC) dataSource.getValue());
}
/**
* Get the formatted value.
*
* @return If the datasource returns null, this is null. Otherwise this is a
* String based on the result of format().
*/
public String toString() {
PC value = dataSource == null ? null : (PC) dataSource.getValue();
if (value == null) {
return null;
}
FC formattedValue = format(value);
return String.valueOf(formattedValue);
}
/**
* Reflects the read-only status of the datasource. If there is no data
* source, returns false.
*/
public boolean isReadOnly() {
return dataSource != null && dataSource.isReadOnly();
}
/**
* This method must be implemented to format the values received from a data
* source for use in a field.
*
* @see #parse(Object)
*
* @param propertyValue
* Value object from the datasource. This is null or of a type
* compatible with getType() of the datasource.
* @return
*/
public abstract FC format(PC propertyValue);
/**
* Parse a value from a field and convert it to format compatible with
* datasource.
*
* The method is required to assure that parse(format(x)) equals x.
*
* @param fieldValue
* field value to convert
* @return value compatible with datasource
* @throws ConversionException
*/
public abstract PC parse(FC fieldValue) throws ConversionException;
/**
* Sets the Property's read-only mode to the specified status.
*
* @param newStatus
* the new read-only status of the Property.
*/
public void setReadOnly(boolean newStatus) {
if (dataSource != null) {
dataSource.setReadOnly(newStatus);
}
}
public void setValue(Object newValue) throws ReadOnlyException,
ConversionException {
if (dataSource == null) {
return;
}
try {
// null is just an ordinary value
PC convertedValue = parse((FC) newValue);
dataSource.setValue(convertedValue);
if (convertedValue == null ? getValue() != null : !convertedValue
.equals(getValue())) {
fireValueChange();
}
} catch (ConversionException e) {
throw e;//just re-throw as is
} catch (Exception e) {
throw new ConversionException(e);
}
}//setValue
// Value change and read-only status listeners and notifications
/**
* An <code>Event</code> object specifying the ObjectProperty whose value
* has changed.
*/
private static class ValueChangeEvent extends java.util.EventObject implements
Property.ValueChangeEvent {
/**
* Constructs a new value change event for this object.
*
* @param source
* the source object of the event.
*/
protected ValueChangeEvent(PropertyConverter<?,?> source) {
super(source);
}//new
public Property getProperty() {
return (Property) getSource();
}
}//ValueChangeEvent
/**
* An <code>Event</code> object specifying the Property whose read-only
* status has been changed.
*/
private static class ReadOnlyStatusChangeEvent extends java.util.EventObject
implements Property.ReadOnlyStatusChangeEvent {
/**
* Constructs a new read-only status change event for this object.
*
* @param source
* source object of the event
*/
protected ReadOnlyStatusChangeEvent(PropertyConverter<?,?> source) {
super(source);
}//new
public Property getProperty() {
return (Property) getSource();
}
}//ReadOnlyStatusChangeEvent
/**
* Removes a previously registered value change listener.
*
* @param listener
* the listener to be removed.
*/
public void removeListener(Property.ValueChangeListener listener) {
valueChangeListeners.remove(listener);
}
/**
* Registers a new value change listener for this ObjectProperty.
*
* @param listener
* the new Listener to be registered
*/
public void addListener(Property.ValueChangeListener listener) {
valueChangeListeners.add(listener);
}
/**
* Registers a new read-only status change listener for this Property.
*
* @param listener
* the new Listener to be registered
*/
public void addListener(Property.ReadOnlyStatusChangeListener listener) {
readOnlyStatusChangeListeners.add(listener);
}
/**
* Removes a previously registered read-only status change listener.
*
* @param listener
* the listener to be removed.
*/
public void removeListener(Property.ReadOnlyStatusChangeListener listener) {
readOnlyStatusChangeListeners.remove(listener);
}
/**
* Sends a value change event to all registered listeners.
*/
protected void fireValueChange() {
final ValueChangeListener[] listeners = valueChangeListeners
.toArray(new ValueChangeListener[valueChangeListeners.size()]);
final Property.ValueChangeEvent event = new ValueChangeEvent(this);
for (ValueChangeListener listener : listeners) {
listener.valueChange(event);
}
}
/**
* Sends a read only status change event to all registered listeners.
*/
protected void fireReadOnlyStatusChange() {
final ReadOnlyStatusChangeListener[] listeners = readOnlyStatusChangeListeners
.toArray(new ReadOnlyStatusChangeListener[readOnlyStatusChangeListeners
.size()]);
final Property.ReadOnlyStatusChangeEvent event = new ReadOnlyStatusChangeEvent(
this);
for (ReadOnlyStatusChangeListener listener : listeners) {
listener.readOnlyStatusChange(event);
}
}
/**
* Listens for changes in the datasource.
*
* This should not be called directly.
*/
public void valueChange(Property.ValueChangeEvent event) {
fireValueChange();
}
/**
* Listens for changes in the datasource.
*
* This should not be called directly.
*/
public void readOnlyStatusChange(Property.ReadOnlyStatusChangeEvent event) {
fireReadOnlyStatusChange();
}
// Validatable
public void addValidator(Validator validator) {
if (validators == null) {
validators = new LinkedList<Validator>();
}
validators.add(validator);
}
public void removeValidator(Validator validator) {
if (validators != null) {
validators.remove(validator);
}
}
public Collection<Validator> getValidators() {
if (validators == null || validators.isEmpty()) {
return Collections.emptySet();//caller friendly
}
return Collections.unmodifiableCollection(validators);
}
@SuppressWarnings("unchecked")
public boolean isValid() {
if (validators == null || dataSource == null) {
return true;
}
final Object value = getPropertyDataSource().getValue();
if (value == null || getType().isAssignableFrom(value.getClass())) {
return isValid((PC) value);
} else {
return false;
}
}
public boolean isValid(PC value) {
if (validators == null) {
return true;
}
for (Validator validator : validators) {
if (!validator.isValid(value)) {
return false;
}
}
return true;
}
@SuppressWarnings("unchecked")
public void validate() throws Validator.InvalidValueException {
if (validators == null || dataSource == null) {
return;
}
final Object value = getPropertyDataSource().getValue();
if (value == null || getType().isAssignableFrom(value.getClass())) {
validate((PC) value);
}
}
public void validate(PC value) throws Validator.InvalidValueException {
// If there is no validator, there can not be any errors
if (validators == null) {
return;
}
// Initialize temps
List<InvalidValueException> errors = new ArrayList<InvalidValueException>();
// validate the underlying value, not the formatted value
// Gets all the validation errors
for (Validator validator : validators) {
try {
validator.validate(value);
} catch (Validator.InvalidValueException e) {
errors.add(e);
}
}
// If there were no error
if (errors.isEmpty()) {
return;
}
// If only one error occurred, throw it forwards
if (errors.size() == 1) {
throw errors.get(0);
}
// Creates composite validation exception
final Validator.InvalidValueException[] exceptions =
errors.toArray(new Validator.InvalidValueException[errors.size()]);
throw new Validator.InvalidValueException(null, exceptions);
}
public boolean isInvalidAllowed() {
return invalidAllowed;
}
public void setInvalidAllowed(boolean invalidAllowed) {
this.invalidAllowed = invalidAllowed;
}
}