/**
* Copyright 2013 Tommi S.E. Laukkanen
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bubblecloud.ilves.component.grid;
import com.vaadin.data.Item;
import com.vaadin.data.Property.ValueChangeEvent;
import com.vaadin.data.Property.ValueChangeListener;
import com.vaadin.data.Validator;
import com.vaadin.data.Validator.InvalidValueException;
import com.vaadin.data.util.converter.Converter;
import com.vaadin.event.FieldEvents.TextChangeEvent;
import com.vaadin.event.FieldEvents.TextChangeListener;
import com.vaadin.server.Resource;
import com.vaadin.server.ThemeResource;
import com.vaadin.shared.ui.MarginInfo;
import com.vaadin.ui.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Data edit form implementation.
*
* @author Tommi S.E. Laukkanen
*/
public class ValidatingEditor extends CustomComponent implements
FormFieldFactory, ValueChangeListener, TextChangeListener {
/** Java serialization version UID. */
private static final long serialVersionUID = 1L;
/** Form component. */
private final Form form;
/** Field definitions defining columns for the grid. */
private final List<FieldDescriptor> fieldDescriptors;
/** Visible properties of the form. */
private final Object[] fieldIds;
/** Visible labels of the fields. */
private final Label[] fieldLabels;
/** Visible labels of the fields. */
private final Field[] fields;
/** Visible status icons of the fields. */
private final Embedded[] fieldIcons;
/** The layout of the form. */
private final GridLayout formLayout;
/** The property field index map. */
private final Map<Field, Integer> fieldIndexes = new HashMap<Field, Integer>();
/** The none getIcon. */
private final ThemeResource noneIcon = new ThemeResource("icons/field-status-icon-none.png");
/** The none getIcon. */
private final ThemeResource newIcon = new ThemeResource("icons/field-status-icon-new.png");
/** The ok getIcon. */
private final ThemeResource validIcon = new ThemeResource("icons/field-status-icon-ok.png");
/** The invalid getIcon. */
private final ThemeResource invalidIcon = new ThemeResource("icons/field-status-icon-invalid.png");
/** The db getIcon. */
private final ThemeResource dbIcon = new ThemeResource("icons/field-status-icon-db.png");
/** True if item edited is new. */
private boolean newItem = false;
/** Validation state change listeners. */
private final List<ValidatingEditorStateListener> listeners = new ArrayList<ValidatingEditorStateListener>();
/** True if value change listener is disabled. */
private boolean disableValueChangeListener = false;
/**
* Constructor which initializes the form.
* @param fieldDescriptors The field definitions.
*/
public ValidatingEditor(final List<FieldDescriptor> fieldDescriptors) {
this.fieldDescriptors = fieldDescriptors;
form = new Form();
form.setBuffered(true);
form.setFormFieldFactory(this);
form.setImmediate(true);
setCompositionRoot(form);
formLayout = new GridLayout(3, fieldDescriptors.size());
formLayout.setSpacing(true);
formLayout.setMargin(new MarginInfo(true, false, true, false));
form.setLayout(formLayout);
fieldIds = new Object[fieldDescriptors.size()];
fields = new Field[fieldDescriptors.size()];
fieldLabels = new Label[fieldDescriptors.size()];
fieldIcons = new Embedded[fieldDescriptors.size()];
for (int i = 0; i < fieldDescriptors.size(); i++) {
final FieldDescriptor fieldDescriptor = fieldDescriptors.get(i);
fieldIds[i] = fieldDescriptor.getId();
fieldLabels[i] = new Label(fieldDescriptor.getLabel());
fieldIcons[i] = new Embedded(null, noneIcon);
fieldIcons[i].setWidth(20, Unit.PIXELS);
fieldIcons[i].setHeight(20, Unit.PIXELS);
formLayout.addComponent(fieldLabels[i], 0, i);
formLayout.addComponent(fieldIcons[i], 2, i);
formLayout.setComponentAlignment(fieldLabels[i], Alignment.MIDDLE_RIGHT);
}
}
/**
* @return the form
*/
public final Form getForm() {
return form;
}
/**
* Sets the Item to be edited.
* @param item the Item to be edited.
* @param newItem true if item being edited is new.
*/
public final void setItem(final Item item, final boolean newItem) {
disableValueChangeListener = true;
if (item != null) {
form.setItemDataSource(item);
form.setVisibleItemProperties(fieldIds);
} else {
form.setItemDataSource(null);
}
disableValueChangeListener = false;
this.newItem = newItem;
refreshFieldState(newItem);
notifyStateChange();
}
/**
* Commits changes to edited item.
*/
public final void commit() {
disableValueChangeListener = true;
form.commit();
disableValueChangeListener = false;
newItem = false;
if (refreshFieldState(false)) {
notifyStateChange();
}
}
/**
* Discards changes to edited item.
*/
public final void discard() {
disableValueChangeListener = true;
form.discard();
disableValueChangeListener = false;
if (refreshFieldState(newItem)) {
notifyStateChange();
}
}
/**
* Gets the Item being edited.
* @return the Item being edited.
*/
public final Item getItem() {
return form.getItemDataSource();
}
/**
* Creates a field based on the item, property id and the component (most
* commonly {@link com.vaadin.ui.Form}) where the Field will be presented.
*
* @param item the item where the property belongs to.
* @param propertyId the Id of the property.
* @param uiContext the component where the field is presented, most
* commonly this is {@link com.vaadin.ui.Form}. uiContext will not necessary be
* the parent component of the field, but the one that is
* responsible for creating it.
* @return Field the field suitable for editing the specified data.
*/
@Override
public final Field createField(final Item item, final Object propertyId, final Component uiContext) {
for (int i = 0; i < fieldDescriptors.size(); i++) {
final FieldDescriptor fieldDefinition = fieldDescriptors.get(i);
if (propertyId.equals(fieldDefinition.getId()) && fieldDefinition.getFieldClass() != null) {
try {
final Field field = (Field) fieldDefinition.getFieldClass().newInstance();
if (field instanceof TextField) {
((TextField) field).setNullRepresentation("");
((TextField) field).setTextChangeTimeout(200);
((TextField) field).addTextChangeListener(this);
}
if (field instanceof TextArea) {
((TextArea) field).setNullRepresentation("");
((TextArea) field).setTextChangeTimeout(200);
((TextArea) field).addTextChangeListener(this);
}
if (field instanceof PasswordField) {
((PasswordField) field).setNullRepresentation("");
((PasswordField) field).setTextChangeTimeout(200);
((PasswordField) field).addTextChangeListener(this);
}
((AbstractField) field).setValidationVisible(false);
for (final Validator validator : fieldDefinition.getValidators()) {
field.addValidator(validator);
}
field.setRequired(fieldDefinition.isRequired());
((AbstractField) field).setConverter(fieldDefinition.getConverter());
field.setPropertyDataSource(null);
if (fieldDefinition.getWidth() > 0) {
field.setWidth(fieldDefinition.getWidth() + 50, Unit.PIXELS);
} else {
field.setWidth(200, Unit.PIXELS);
}
field.setReadOnly(isReadOnly() || fieldDefinition.isReadOnly());
if (!fieldDefinition.isReadOnly()) {
field.setValue(fieldDefinition.getDefaultValue());
}
field.addValueChangeListener(this);
((AbstractComponent) field).setImmediate(true);
formLayout.setCursorX(1);
formLayout.setCursorY(i);
fieldIndexes.put(field, i);
fields[i] = field;
return field;
} catch (final Throwable t) {
throw new RuntimeException("Error instantiating field: " + propertyId, t);
}
}
}
return null;
}
/**
* Can be used to validate the contents of a field. This can be used to
* invoke validation of dependent field from another validator.
*
* @param propertyId the property ID
*/
public final void validateField(final Object propertyId) {
for (int i = 0; i < fieldDescriptors.size(); i++) {
if (fieldDescriptors.get(i).getId().equals(propertyId)) {
refreshFieldState(i, newItem);
notifyStateChange();
}
}
}
/**
* Refreshes field icons.
* @param newItem true if item being edited is new.
* @return true if field state changed.
*/
private boolean refreshFieldState(final boolean newItem) {
boolean stateChange = false;
for (int i = 0; i < fieldDescriptors.size(); i++) {
if (refreshFieldState(i, newItem)) {
stateChange = true;
}
}
return stateChange;
}
/**
* Notify state change to listeners.
*/
public final void notifyStateChange() {
for (final ValidatingEditorStateListener listener : listeners) {
listener.editorStateChanged(this);
}
}
/**
* Updates field state getIcon of given field index.
* @param fieldIndex index of the field.
* @param newItem true if item being edited is new.
* @return true if field state changed.
*/
public final boolean refreshFieldState(final int fieldIndex, final boolean newItem) {
final Resource currentIcon = fieldIcons[fieldIndex].getSource();
if (fields[fieldIndex].isReadOnly()) {
fieldIcons[fieldIndex].setSource(noneIcon);
} else if (newItem) {
if (fields[fieldIndex] instanceof CheckBox) {
fieldIcons[fieldIndex].setSource(dbIcon);
} else {
if (fields[fieldIndex].isValid()) {
fieldIcons[fieldIndex].setSource(dbIcon);
fieldIcons[fieldIndex].setDescription("");
} else {
fieldIcons[fieldIndex].setSource(newIcon);
}
}
} else if (fields[fieldIndex].isModified()) {
if (fields[fieldIndex].isValid()) {
fieldIcons[fieldIndex].setSource(validIcon);
fieldIcons[fieldIndex].setDescription("");
} else {
fieldIcons[fieldIndex].setSource(invalidIcon);
try {
fields[fieldIndex].validate();
} catch (final InvalidValueException e) {
fieldIcons[fieldIndex].setDescription(e.getMessage());
}
}
} else {
fieldIcons[fieldIndex].setSource(dbIcon);
}
return fieldIcons[fieldIndex].getSource() != currentIcon;
}
/**
* Adds state change listener.
* @param listener the listener to add.
*/
public final void addListener(final ValidatingEditorStateListener listener) {
listeners.add(listener);
}
/**
* Removes state change listener.
* @param listener the listener to remove.
*/
public final void removeListener(final ValidatingEditorStateListener listener) {
listeners.remove(listener);
}
/**
* Does form contain modified field values.
* @return true if modifications exist.
*/
public final boolean isModified() {
for (int i = 0; i < fieldIcons.length; i++) {
final Resource icon = fieldIcons[i].getSource();
if (icon == validIcon || icon == invalidIcon) {
return true;
}
}
return false;
}
/**
* Are all form field values valid.
* @return true if all field values are valid.
*/
public final boolean isValid() {
for (int i = 0; i < fieldIcons.length; i++) {
final Resource icon = fieldIcons[i].getSource();
if (icon != validIcon && icon != noneIcon && icon != dbIcon) {
return false;
}
}
return true;
}
/**
* @return true if the item edited is new.
*/
public final boolean isNewItem() {
return newItem;
}
@Override
public final void valueChange(final ValueChangeEvent event) {
if (!disableValueChangeListener) {
if (refreshFieldState(fieldIndexes.get(event.getProperty()), false)) {
notifyStateChange();
}
}
}
@Override
public final void textChange(final TextChangeEvent event) {
boolean valid = true;
final int fieldIndex = fieldIndexes.get((Field) event.getComponent());
if (fields[fieldIndex].getValidators() != null) {
if ((event.getText() == null || event.getText().length() == 0) &&
fields[fieldIndex].isRequired()) {
fieldIcons[fieldIndex].setDescription("");
valid = false;
} else {
for (final Validator validator : fields[fieldIndex].getValidators()) {
try {
final Converter converter = ((AbstractField) fields[fieldIndex]).getConverter();
final Object value;
if (converter != null) {
value = converter.convertToModel(event.getText(), converter.getModelType(),
((AbstractField) fields[fieldIndex]).getLocale());
} else {
value = event.getText();
}
validator.validate(value);
fieldIcons[fieldIndex].setDescription("");
} catch (final InvalidValueException e) {
fieldIcons[fieldIndex].setDescription(e.getMessage());
valid = false;
} catch (final Exception e) {
fieldIcons[fieldIndex].setDescription("");
valid = false;
}
}
}
}
final Resource originalSource = fieldIcons[fieldIndex].getSource();
if (valid) {
fieldIcons[fieldIndex].setSource(validIcon);
} else {
fieldIcons[fieldIndex].setSource(invalidIcon);
}
if (originalSource != fieldIcons[fieldIndex].getSource()) {
notifyStateChange();
}
}
@Override
public final void setCaption(final String caption) {
form.setCaption(caption);
}
}