org.dellroad.stuff.vaadin7.FieldBuilder Maven / Gradle / Ivy
Show all versions of dellroad-stuff-vaadin7 Show documentation
/*
* Copyright (C) 2022 Archie L. Cobbs. All rights reserved.
*/
package org.dellroad.stuff.vaadin7;
import com.vaadin.data.Validator;
import com.vaadin.data.fieldgroup.BeanFieldGroup;
import com.vaadin.data.util.BeanItem;
import com.vaadin.data.util.converter.Converter;
import com.vaadin.shared.ui.combobox.FilteringMode;
import com.vaadin.shared.ui.datefield.Resolution;
import com.vaadin.ui.AbstractField;
import com.vaadin.ui.AbstractSelect;
import com.vaadin.ui.AbstractTextField;
import com.vaadin.ui.CheckBox;
import com.vaadin.ui.ComboBox;
import com.vaadin.ui.DateField;
import com.vaadin.ui.Field;
import com.vaadin.ui.ListSelect;
import com.vaadin.ui.PasswordField;
import com.vaadin.ui.TextArea;
import com.vaadin.ui.TextField;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import org.dellroad.stuff.java.MethodAnnotationScanner;
/**
* Automatically builds and binds fields for a Java bean annotated with {@link FieldBuilder} annotations.
*
*
*
*
*
* The various nested {@link FieldBuilder} annotation types annotate Java bean property "getter" methods and specify
* how the the bean properties of that class should be edited using {@link AbstractField}s. This allows all information
* about how to edit a Java model class to stay contained within that class.
*
*
* This class supports two types of annotations: first, the {@link ProvidesField @ProvidesField} annotation annotates
* a method that knows how to build an {@link AbstractField} suitable for editing the bean property specified by
* its {@link ProvidesField#value value()}. So {@link ProvidesField @ProvidesField} is analgous to
* {@link ProvidesProperty @ProvidesProperty}, except that it defines an editing field rather than a container property.
*
*
* The {@link FieldBuilder.AbstractField @FieldBuilder.AbstractField} hierarchy annotations are the other type of annotation.
* These annotations annotate a Java bean property getter method and specify how to configure an {@link AbstractField} subclass
* instance to edit the bean property corresponding to the getter method.
* {@link FieldBuilder.AbstractField @FieldBuilder.AbstractField} is the top level annotation in a hierarchy of annotations
* that correspond to the {@link AbstractField} class hierarchy. {@link FieldBuilder.AbstractField @FieldBuilder.AbstractField}
* corresponds to {@link AbstractField}, and its properties configure corresponding {@link AbstractField} properties.
* More specific annotations correspond to the various {@link AbstractField} subclasses,
* for example {@link ComboBox @FieldBuilder.ComboBox} corresponds to {@link ComboBox}.
* When using more specific annotations, the "superclass" annotations configure the corresponding superclass' properties.
*
*
* A simple example shows how these annotations are used:
*
*
* // Use a 10x40 TextArea to edit the "description" property
* @FieldBuilder.AbstractField(caption = "Description:")
* @FieldBuilder.TextArea(columns = 40, rows = 10)
* public String getDescription() {
* return this.description;
* }
*
* // Use my own custom field to edit the "foobar" property
* @FieldBuilder.ProvidesField("foobar")
* private MyCustomField createFoobarField() {
* ...
* }
*
*
*
* A {@link FieldBuilder} instance will read these annotations and build the fields automatically. For example:
*
*
* // Create fields based on FieldGroup.* annotations
* Person person = new Person("Joe Smith", 100);
* BeanFieldGroup<Person> fieldGroup = FieldBuilder.buildFieldGroup(person);
*
* // Layout the fields in a form
* FormLayout layout = new FormLayout();
* for (Field<?> field : fieldGroup.getFields())
* layout.addComponent(field);
*
*
*
* For all annotations in the {@link FieldBuilder.AbstractField @FieldBuilder.AbstractField} hierarchy, leaving properties
* set to their default values results in the default behavior.
*
* @see AbstractSelect
* @see AbstractTextField
* @see CheckBox
* @see ComboBox
* @see DateField
* @see EnumComboBox
* @see ListSelect
* @see PasswordField
* @see TextArea
* @see TextField
*/
public class FieldBuilder {
/**
* Introspect for {@link FieldBuilder} annotations on property getter methods of the
* {@link BeanFieldGroup}'s data source, and then build and bind the corresponding fields.
*
* @param fieldGroup field group to configure
* @throws IllegalArgumentException if {@code fieldGroup} is null
* @throws IllegalArgumentException if {@code fieldGroup} does not yet
* {@linkplain BeanFieldGroup#setItemDataSource(Object) have a data source}
*/
public void buildAndBind(BeanFieldGroup> fieldGroup) {
// Sanity check
if (fieldGroup == null)
throw new IllegalArgumentException("null beanType");
final BeanItem> beanItem = fieldGroup.getItemDataSource();
if (beanItem == null)
throw new IllegalArgumentException("fieldGroup does not yet have a data source");
// Scan bean properties to build fields
for (Map.Entry> entry : this.buildBeanPropertyFields(beanItem.getBean()).entrySet())
fieldGroup.bind(entry.getValue(), entry.getKey());
}
/**
* Introspect for {@link FieldBuilder} annotations on property getter methods and build
* a mapping from Java bean property name to a field that may be used to edit that property.
* Only bean properties that have {@link FieldBuilder} annotations are detected.
*
* @param bean Java bean
* @return mapping from bean property name to field
* @throws IllegalArgumentException if {@code bean} is null
* @throws IllegalArgumentException if invalid or conflicting annotations are encountered
*/
public Map> buildBeanPropertyFields(Object bean) {
// Sanity check
if (bean == null)
throw new IllegalArgumentException("null bean");
// Look for all bean property getter methods
BeanInfo beanInfo;
try {
beanInfo = Introspector.getBeanInfo(bean.getClass());
} catch (IntrospectionException e) {
throw new RuntimeException("unexpected exception", e);
}
final HashMap getterMap = new HashMap<>(); // contains all getter methods
for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {
Method method = propertyDescriptor.getReadMethod();
// Work around Introspector stupidity where it returns overridden superclass' getter method
if (method != null && method.getClass() != bean.getClass()) {
for (Class> c = bean.getClass(); c != null && c != method.getClass(); c = c.getSuperclass()) {
try {
method = c.getDeclaredMethod(method.getName(), method.getParameterTypes());
} catch (Exception e) {
continue;
}
break;
}
}
// Add getter, if appropriate
if (method != null && method.getReturnType() != void.class && method.getParameterTypes().length == 0)
getterMap.put(propertyDescriptor.getName(), method);
}
// Scan getters for FieldBuilder.* annotations other than FieldBuidler.ProvidesField
final HashMap> map = new HashMap<>(); // contains @FieldBuilder.* fields
for (Map.Entry entry : getterMap.entrySet()) {
final String propertyName = entry.getKey();
final Method method = entry.getValue();
// Get annotations, if any
final List> applierList = this.buildApplierList(method);
if (applierList.isEmpty())
continue;
// Build field
map.put(propertyName, this.buildField(applierList, "method " + method));
}
// Scan all methods for @FieldBuilder.ProvidesField annotations
final HashMap providerMap = new HashMap<>(); // contains @FieldBuilder.ProvidesField methods
this.buildProviderMap(providerMap, bean.getClass());
// Check for conflicts between @FieldBuidler.ProvidesField and other annotations and add fields to map
for (Map.Entry entry : providerMap.entrySet()) {
final String propertyName = entry.getKey();
final Method method = entry.getValue();
// Verify field is not already defined
if (map.containsKey(propertyName)) {
throw new IllegalArgumentException("conflicting annotations exist for property `" + propertyName + "': annotation @"
+ ProvidesField.class.getName() + " on method " + method
+ " cannot be combined with other @FieldBuilder.* annotation types");
}
// Invoke method to create field
Field> field;
try {
method.setAccessible(true);
} catch (RuntimeException e) {
// ignore
}
try {
field = (Field>)method.invoke(bean);
} catch (Exception e) {
throw new RuntimeException("error invoking @" + ProvidesField.class.getName()
+ " annotation on method " + method, e);
}
// Save field
map.put(propertyName, field);
}
// Done
return map;
}
// This method exists solely to bind the generic type
private void buildProviderMap(Map providerMap, Class type) {
final MethodAnnotationScanner scanner = new MethodAnnotationScanner<>(type, ProvidesField.class);
for (MethodAnnotationScanner.MethodInfo methodInfo : scanner.findAnnotatedMethods())
this.buildProviderMap(providerMap, methodInfo.getMethod().getDeclaringClass(), methodInfo.getMethod().getName());
}
// Used by buildBeanPropertyFields() to validate @FieldBuilder.ProvidesField annotations
private void buildProviderMap(Map providerMap, Class> type, String methodName) {
// Terminate recursion
if (type == null)
return;
// Check the method in this class
do {
// Get method
Method method;
try {
method = type.getDeclaredMethod(methodName);
} catch (NoSuchMethodException e) {
break;
}
// Get annotation
final ProvidesField providesField = method.getAnnotation(ProvidesField.class);
if (providesField == null)
break;
// Validate method return type is compatible with Field
if (!Field.class.isAssignableFrom(method.getReturnType())) {
throw new IllegalArgumentException("invalid @" + ProvidesField.class.getName() + " annotation on method " + method
+ ": return type " + method.getReturnType().getName() + " is not a subtype of " + Field.class.getName());
}
// Check for two methods declaring fields for the same property
final String propertyName = providesField.value();
final Method otherMethod = providerMap.get(propertyName);
if (otherMethod != null && !otherMethod.getName().equals(methodName)) {
throw new IllegalArgumentException("conflicting @" + ProvidesField.class.getName()
+ " annotations exist for property `" + propertyName + "': both method "
+ otherMethod + " and method " + method + " are specified");
}
// Save method
if (otherMethod == null)
providerMap.put(propertyName, method);
} while (false);
// Recurse on interfaces
for (Class> iface : type.getInterfaces())
this.buildProviderMap(providerMap, iface, methodName);
// Recurse on superclass
this.buildProviderMap(providerMap, type.getSuperclass(), methodName);
}
/**
* Create a {@link BeanFieldGroup} using the given instance, introspect for {@link FieldBuilder} annotations
* on property getter methods of the given bean's class, and build and bind the corresponding fields, and return
* the result.
*
* @param bean model bean annotated with {@link FieldBuilder} annotations
* @param bean type
* @return new {@link BeanFieldGroup}
* @throws IllegalArgumentException if {@code bean} is null
*/
@SuppressWarnings("unchecked")
public static BeanFieldGroup buildFieldGroup(T bean) {
// Sanity check
if (bean == null)
throw new IllegalArgumentException("null bean");
// Create field group
final BeanFieldGroup fieldGroup = new BeanFieldGroup<>((Class)bean.getClass());
fieldGroup.setItemDataSource(bean);
new FieldBuilder().buildAndBind(fieldGroup);
return fieldGroup;
}
/**
* Instantiate and configure an {@link AbstractField} according to the given scanned annotations.
*
* @param appliers annotation appliers
* @param description description of the field (used for exception messages)
* @return new field
*/
protected com.vaadin.ui.AbstractField> buildField(Collection> appliers, String description) {
// Get comparator that sorts by class hierarcy, narrower types first; note Collections.sort() is stable,
// so for any specific annotation type, that annotation on subtype appears before that annotation on supertype.
final Comparator> comparator = new Comparator>() {
@Override
public int compare(AnnotationApplier, ?> a1, AnnotationApplier, ?> a2) {
final Class extends com.vaadin.ui.AbstractField>> type1 = a1.getFieldType();
final Class extends com.vaadin.ui.AbstractField>> type2 = a2.getFieldType();
if (type1 == type2)
return 0;
if (type1.isAssignableFrom(type2))
return 1;
if (type2.isAssignableFrom(type1))
return -1;
return 0;
}
};
// Sanity check for duplicates and conflicts
final ArrayList> applierList = new ArrayList<>(appliers);
Collections.sort(applierList, comparator);
for (int i = 0; i < applierList.size() - 1; ) {
final AnnotationApplier, ?> a1 = applierList.get(i);
final AnnotationApplier, ?> a2 = applierList.get(i + 1);
// Let annotations on subclass override annotations on superclass
if (a1.getAnnotation().annotationType() == a2.getAnnotation().annotationType()) {
applierList.remove(i + 1);
continue;
}
// Check for conflicting annotation types (e.g., both FieldBuilder.TextField and FieldBuilder.DateField)
if (comparator.compare(a1, a2) == 0) {
throw new IllegalArgumentException("conflicting annotations of type "
+ a1.getAnnotation().annotationType().getName() + " and " + a2.getAnnotation().annotationType().getName()
+ " for " + description);
}
i++;
}
// Determine field type
Class extends com.vaadin.ui.AbstractField>> type = null;
AnnotationApplier, ?> typeApplier = null;
for (AnnotationApplier, ?> applier : applierList) {
// Pick up type() if specified
if (applier.getActualFieldType() == null)
continue;
if (type == null) {
type = applier.getActualFieldType();
typeApplier = applier;
continue;
}
// Verify the field type specified by a narrower annotation has compatible narrower field type
if (!applier.getActualFieldType().isAssignableFrom(type) && typeApplier != null) {
throw new IllegalArgumentException("conflicting field types specified by annotations of type "
+ typeApplier.getAnnotation().annotationType().getName() + " (type() = " + type.getName() + ") and "
+ applier.getAnnotation().annotationType().getName() + " (type() = " + applier.getActualFieldType().getName()
+ ") for " + description);
}
}
if (type == null)
throw new IllegalArgumentException("cannot determine field type; no type() specified for " + description);
// Instantiate field
final com.vaadin.ui.AbstractField> field = FieldBuilder.instantiate(type);
// Configure the field
for (AnnotationApplier, ?> applier : applierList)
this.apply(applier, field);
// Done
return field;
}
// This method exists solely to bind the generic type
private > void apply(AnnotationApplier, F> applier,
com.vaadin.ui.AbstractField> field) {
applier.applyTo(applier.getFieldType().cast(field));
}
private static T instantiate(Class type) {
Constructor constructor;
try {
constructor = type.getDeclaredConstructor();
} catch (ReflectiveOperationException e) {
throw new RuntimeException("cannot instantiate " + type + " because no zero-arg constructor could be found", e);
}
try {
constructor.setAccessible(true);
} catch (RuntimeException e) {
// ignore
}
try {
return constructor.newInstance();
} catch (ReflectiveOperationException e) {
throw new RuntimeException("cannot instantiate " + type + " using its zero-arg constructor", e);
}
}
/**
* Find all relevant annotations on the given method as well as on any supertype methods it overrides.
* The method must be a getter method taking no arguments. Annotations are ordered so that annotations
* on a method in type X appear before annotations on an overridden method in type Y, a supertype of X.
*
* @param method annotated getter method
* @return appliers for annotations found
* @throws IllegalArgumentException if {@code method} is null
* @throws IllegalArgumentException if {@code method} has parameters
*/
protected List> buildApplierList(Method method) {
// Sanity check
if (method == null)
throw new IllegalArgumentException("null method");
if (method.getParameterTypes().length > 0)
throw new IllegalArgumentException("method takes parameters");
// Recurse
final ArrayList> list = new ArrayList<>();
this.buildApplierList(method.getDeclaringClass(), method.getName(), list);
return list;
}
private void buildApplierList(Class> type, String methodName, List> list) {
// Terminate recursion
if (type == null)
return;
// Check class
Method method;
try {
method = type.getMethod(methodName);
} catch (NoSuchMethodException e) {
method = null;
}
if (method != null)
list.addAll(this.buildDirectApplierList(method));
// Recurse on interfaces
for (Class> iface : type.getInterfaces())
this.buildApplierList(iface, methodName, list);
// Recurse on superclass
this.buildApplierList(type.getSuperclass(), methodName, list);
}
/**
* Find all relevant annotations declared directly on the given {@link Method}.
*
* @param method method to inspect
* @return annotations found
* @throws IllegalArgumentException if {@code method} is null
*/
protected List> buildDirectApplierList(Method method) {
// Sanity check
if (method == null)
throw new IllegalArgumentException("null method");
// Build list
final ArrayList> list = new ArrayList<>();
for (Annotation annotation : method.getDeclaredAnnotations()) {
final AnnotationApplier extends Annotation, ?> applier = this.getAnnotationApplier(method, annotation);
if (applier != null)
list.add(applier);
}
return list;
}
/**
* Get the {@link AnnotationApplier} that applies the given annotation.
* Subclasses can add support for additional annotation types by overriding this method.
*
* @param method method to inspect
* @param annotation method annotation to inspect
* @return corresponding {@link AnnotationApplier}, or null if annotation is unknown
*/
protected AnnotationApplier, ?> getAnnotationApplier(Method method, Annotation annotation) {
if (annotation instanceof FieldBuilder.AbstractField)
return new AbstractFieldApplier(method, (FieldBuilder.AbstractField)annotation);
if (annotation instanceof FieldBuilder.AbstractSelect)
return new AbstractSelectApplier(method, (FieldBuilder.AbstractSelect)annotation);
if (annotation instanceof FieldBuilder.CheckBox)
return new CheckBoxApplier(method, (FieldBuilder.CheckBox)annotation);
if (annotation instanceof FieldBuilder.ComboBox)
return new ComboBoxApplier(method, (FieldBuilder.ComboBox)annotation);
if (annotation instanceof FieldBuilder.EnumComboBox)
return new EnumComboBoxApplier(method, (FieldBuilder.EnumComboBox)annotation);
if (annotation instanceof FieldBuilder.ListSelect)
return new ListSelectApplier(method, (FieldBuilder.ListSelect)annotation);
if (annotation instanceof FieldBuilder.DateField)
return new DateFieldApplier(method, (FieldBuilder.DateField)annotation);
if (annotation instanceof FieldBuilder.AbstractTextField)
return new AbstractTextFieldApplier(method, (FieldBuilder.AbstractTextField)annotation);
if (annotation instanceof FieldBuilder.TextField)
return new TextFieldApplier(method, (FieldBuilder.TextField)annotation);
if (annotation instanceof FieldBuilder.TextArea)
return new TextAreaApplier(method, (FieldBuilder.TextArea)annotation);
if (annotation instanceof FieldBuilder.PasswordField)
return new PasswordFieldApplier(method, (FieldBuilder.PasswordField)annotation);
return null;
}
// AnnotationApplier
/**
* Class that knows how to apply annotation properties to a corresponding field.
*/
protected abstract static class AnnotationApplier> {
protected final Method method;
protected final A annotation;
protected final Class fieldType;
protected AnnotationApplier(Method method, A annotation, Class fieldType) {
if (method == null)
throw new IllegalArgumentException("null method");
if (annotation == null)
throw new IllegalArgumentException("null annotation");
if (fieldType == null)
throw new IllegalArgumentException("null fieldType");
this.method = method;
this.annotation = annotation;
this.fieldType = fieldType;
}
public final Method getMethod() {
return this.method;
}
public final A getAnnotation() {
return this.annotation;
}
public final Class getFieldType() {
return this.fieldType;
}
public abstract Class extends F> getActualFieldType();
public abstract void applyTo(F field);
}
/**
* Applies properties from a {@link FieldBuilder.AbstractField} annotation to a {@link com.vaadin.ui.AbstractField}.
*/
private static class AbstractFieldApplier
extends AnnotationApplier> {
@SuppressWarnings("unchecked")
AbstractFieldApplier(Method method, FieldBuilder.AbstractField annotation) {
super(method, annotation, (Class>)(Object)com.vaadin.ui.AbstractField.class);
}
@Override
@SuppressWarnings("unchecked")
public Class extends com.vaadin.ui.AbstractField>> getActualFieldType() {
return (Class>)(Object)(this.annotation.type() != com.vaadin.ui.AbstractField.class ?
this.annotation.type() : null);
}
@Override
@SuppressWarnings("unchecked")
public void applyTo(com.vaadin.ui.AbstractField> field) {
if (this.annotation.width().length() > 0)
field.setWidth(this.annotation.width());
if (this.annotation.height().length() > 0)
field.setHeight(this.annotation.height());
if (this.annotation.caption().length() > 0)
field.setCaption(this.annotation.caption());
if (this.annotation.description().length() > 0)
field.setDescription(this.annotation.description());
field.setEnabled(this.annotation.enabled());
field.setImmediate(this.annotation.immediate());
field.setReadOnly(this.annotation.readOnly());
field.setBuffered(this.annotation.buffered());
field.setInvalidAllowed(this.annotation.invalidAllowed());
field.setInvalidCommitted(this.annotation.invalidCommitted());
field.setValidationVisible(this.annotation.validationVisible());
field.setRequired(this.annotation.required());
field.setTabIndex(this.annotation.tabIndex());
if (this.annotation.converter() != Converter.class)
field.setConverter(FieldBuilder.instantiate(this.annotation.converter()));
for (Class extends Validator> validatorType : this.annotation.validators())
field.addValidator(FieldBuilder.instantiate(validatorType));
for (String styleName : this.annotation.styleNames())
field.addStyleName(styleName);
if (this.annotation.conversionError().length() > 0)
field.setConversionError(this.annotation.conversionError());
if (this.annotation.requiredError().length() > 0)
field.setRequiredError(this.annotation.requiredError());
}
}
/**
* Applies properties from a {@link FieldBuilder.AbstractSelect} annotation to a {@link com.vaadin.ui.AbstractSelect}.
*/
private static class AbstractSelectApplier
extends AnnotationApplier {
AbstractSelectApplier(Method method, FieldBuilder.AbstractSelect annotation) {
super(method, annotation, com.vaadin.ui.AbstractSelect.class);
}
@Override
public Class extends com.vaadin.ui.AbstractSelect> getActualFieldType() {
return this.annotation.type() != com.vaadin.ui.AbstractSelect.class ? this.annotation.type() : null;
}
@Override
public void applyTo(com.vaadin.ui.AbstractSelect field) {
field.setItemCaptionMode(this.annotation.itemCaptionMode());
if (this.annotation.itemCaptionPropertyId().length() > 0)
field.setItemCaptionPropertyId(this.annotation.itemCaptionPropertyId());
if (this.annotation.itemIconPropertyId().length() > 0)
field.setItemIconPropertyId(this.annotation.itemIconPropertyId());
if (this.annotation.nullSelectionItemId().length() > 0)
field.setNullSelectionItemId(this.annotation.nullSelectionItemId());
field.setMultiSelect(this.annotation.multiSelect());
field.setNewItemsAllowed(this.annotation.newItemsAllowed());
field.setNullSelectionAllowed(this.annotation.nullSelectionAllowed());
}
}
/**
* Applies properties from a {@link FieldBuilder.CheckBox} annotation to a {@link com.vaadin.ui.CheckBox}.
*/
private static class CheckBoxApplier extends AnnotationApplier {
CheckBoxApplier(Method method, FieldBuilder.CheckBox annotation) {
super(method, annotation, com.vaadin.ui.CheckBox.class);
}
@Override
public Class extends com.vaadin.ui.CheckBox> getActualFieldType() {
return this.annotation.type();
}
@Override
public void applyTo(com.vaadin.ui.CheckBox field) {
}
}
/**
* Applies properties from a {@link FieldBuilder.ComboBox} annotation to a {@link com.vaadin.ui.ComboBox}.
*/
private static class ComboBoxApplier extends AnnotationApplier {
ComboBoxApplier(Method method, FieldBuilder.ComboBox annotation) {
super(method, annotation, com.vaadin.ui.ComboBox.class);
}
@Override
public Class extends com.vaadin.ui.ComboBox> getActualFieldType() {
return this.annotation.type();
}
@Override
public void applyTo(com.vaadin.ui.ComboBox field) {
if (this.annotation.inputPrompt().length() > 0)
field.setInputPrompt(this.annotation.inputPrompt());
if (this.annotation.pageLength() != -1)
field.setPageLength(this.annotation.pageLength());
field.setScrollToSelectedItem(this.annotation.scrollToSelectedItem());
field.setTextInputAllowed(this.annotation.textInputAllowed());
field.setFilteringMode(this.annotation.filteringMode());
}
}
/**
* Applies properties from a {@link FieldBuilder.EnumComboBox} annotation to a {@link org.dellroad.stuff.vaadin7.EnumComboBox}.
*/
private static class EnumComboBoxApplier
extends AnnotationApplier {
EnumComboBoxApplier(Method method, FieldBuilder.EnumComboBox annotation) {
super(method, annotation, org.dellroad.stuff.vaadin7.EnumComboBox.class);
}
@Override
public Class extends org.dellroad.stuff.vaadin7.EnumComboBox> getActualFieldType() {
return this.annotation.type();
}
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public void applyTo(org.dellroad.stuff.vaadin7.EnumComboBox field) {
Class extends Enum> enumClass = this.annotation.enumClass();
if (enumClass == Enum.class) {
try {
enumClass = this.method.getReturnType().asSubclass(Enum.class);
} catch (ClassCastException e) {
throw new IllegalArgumentException("invalid @EnumComboBox annotation on non-Enum method " + this.getMethod());
}
}
field.setEnumDataSource(enumClass);
}
}
/**
* Applies properties from a {@link FieldBuilder.ListSelect} annotation to a {@link com.vaadin.ui.ListSelect}.
*/
private static class ListSelectApplier extends AnnotationApplier {
ListSelectApplier(Method method, FieldBuilder.ListSelect annotation) {
super(method, annotation, com.vaadin.ui.ListSelect.class);
}
@Override
public Class extends com.vaadin.ui.ListSelect> getActualFieldType() {
return this.annotation.type();
}
@Override
public void applyTo(com.vaadin.ui.ListSelect field) {
if (this.annotation.rows() != -1)
field.setRows(this.annotation.rows());
}
}
/**
* Applies properties from a {@link FieldBuilder.DateField} annotation to a {@link com.vaadin.ui.DateField}.
*/
private static class DateFieldApplier extends AnnotationApplier {
DateFieldApplier(Method method, FieldBuilder.DateField annotation) {
super(method, annotation, com.vaadin.ui.DateField.class);
}
@Override
public Class extends com.vaadin.ui.DateField> getActualFieldType() {
return this.annotation.type();
}
@Override
public void applyTo(com.vaadin.ui.DateField field) {
if (this.annotation.dateFormat().length() > 0)
field.setDateFormat(this.annotation.dateFormat());
if (this.annotation.parseErrorMessage().length() > 0)
field.setParseErrorMessage(this.annotation.parseErrorMessage());
if (this.annotation.dateOutOfRangeMessage().length() > 0)
field.setDateOutOfRangeMessage(this.annotation.dateOutOfRangeMessage());
field.setResolution(this.annotation.resolution());
field.setShowISOWeekNumbers(this.annotation.showISOWeekNumbers());
if (this.annotation.timeZone().length() > 0)
field.setTimeZone(TimeZone.getTimeZone(this.annotation.timeZone()));
field.setLenient(this.annotation.lenient());
}
}
/**
* Applies properties from a {@link FieldBuilder.AbstractTextField} annotation to a {@link com.vaadin.ui.AbstractTextField}.
*/
private static class AbstractTextFieldApplier
extends AnnotationApplier {
AbstractTextFieldApplier(Method method, FieldBuilder.AbstractTextField annotation) {
super(method, annotation, com.vaadin.ui.AbstractTextField.class);
}
@Override
public Class extends com.vaadin.ui.AbstractTextField> getActualFieldType() {
return this.annotation.type();
}
@Override
public void applyTo(com.vaadin.ui.AbstractTextField field) {
field.setNullRepresentation(this.annotation.nullRepresentation());
field.setNullSettingAllowed(this.annotation.nullSettingAllowed());
field.setTextChangeEventMode(this.annotation.textChangeEventMode());
field.setTextChangeTimeout(this.annotation.textChangeTimeout());
if (this.annotation.inputPrompt().length() > 0)
field.setInputPrompt(this.annotation.inputPrompt());
field.setColumns(this.annotation.columns());
if (this.annotation.maxLength() != -1)
field.setMaxLength(this.annotation.maxLength());
}
}
/**
* Applies properties from a {@link FieldBuilder.TextField} annotation to a {@link com.vaadin.ui.TextField}.
*/
private static class TextFieldApplier extends AnnotationApplier {
TextFieldApplier(Method method, FieldBuilder.TextField annotation) {
super(method, annotation, com.vaadin.ui.TextField.class);
}
@Override
public Class extends com.vaadin.ui.TextField> getActualFieldType() {
return this.annotation.type();
}
@Override
public void applyTo(com.vaadin.ui.TextField field) {
}
}
/**
* Applies properties from a {@link FieldBuilder.TextArea} annotation to a {@link com.vaadin.ui.TextArea}.
*/
private static class TextAreaApplier extends AnnotationApplier {
TextAreaApplier(Method method, FieldBuilder.TextArea annotation) {
super(method, annotation, com.vaadin.ui.TextArea.class);
}
@Override
public Class extends com.vaadin.ui.TextArea> getActualFieldType() {
return this.annotation.type();
}
@Override
public void applyTo(com.vaadin.ui.TextArea field) {
field.setWordwrap(this.annotation.wordwrap());
if (this.annotation.rows() != -1)
field.setRows(this.annotation.rows());
}
}
/**
* Applies properties from a {@link FieldBuilder.PasswordField} annotation to a {@link com.vaadin.ui.PasswordField}.
*/
private static class PasswordFieldApplier extends AnnotationApplier {
PasswordFieldApplier(Method method, FieldBuilder.PasswordField annotation) {
super(method, annotation, com.vaadin.ui.PasswordField.class);
}
@Override
public Class extends com.vaadin.ui.PasswordField> getActualFieldType() {
return this.annotation.type();
}
@Override
public void applyTo(com.vaadin.ui.PasswordField field) {
}
}
// Annotations
/**
* Specifies that the annotated method will return an {@link com.vaadin.ui.AbstractField} suitable for
* editing the specified property.
*
*
* Annotated methods must take zero arguments and return a type compatible with {@link com.vaadin.ui.AbstractField}.
*
* @see FieldBuilder
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface ProvidesField {
/**
* The name of the property that the annotated method's return value edits.
*
* @return property name
*/
String value();
}
/**
* Specifies how a Java property should be edited in Vaadin using an {@link com.vaadin.ui.AbstractField}.
*
* @see FieldBuilder
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface AbstractField {
/**
* Get the {@link AbstractField} type that will edit the property.
*
*
* Although this property has a default value, it must be overridden either in this annotation, or
* by also including a more specific annotation such as {@link TextField}.
*
* @return field type
*/
@SuppressWarnings("rawtypes")
Class extends com.vaadin.ui.AbstractField> type() default com.vaadin.ui.AbstractField.class;
/**
* Get style names.
*
* @return style names
* @see com.vaadin.ui.AbstractComponent#addStyleName
*/
String[] styleNames() default {};
/**
* Get width.
*
* @return field width
* @see com.vaadin.ui.AbstractComponent#setWidth(String)
*/
String width() default "";
/**
* Get height.
*
* @return field height
* @see com.vaadin.ui.AbstractComponent#setHeight(String)
*/
String height() default "";
/**
* Get the caption associated with this field.
*
* @return field caption
* @see com.vaadin.ui.AbstractComponent#setCaption
*/
String caption() default "";
/**
* Get the description associated with this field.
*
* @return field description
* @see com.vaadin.ui.AbstractComponent#setDescription
*/
String description() default "";
/**
* Get whether this field is enabled.
*
* @return true to enable field
* @see com.vaadin.ui.AbstractComponent#setEnabled
*/
boolean enabled() default true;
/**
* Get whether this field is immediate.
*
* @return true for immediate mode
* @see com.vaadin.ui.AbstractComponent#setImmediate
*/
boolean immediate() default false;
/**
* Get whether this field is read-only.
*
* @return true for read-only
* @see com.vaadin.ui.AbstractComponent#setReadOnly
*/
boolean readOnly() default false;
/**
* Get the {@link Converter} type that convert field value to data model type.
* The specified class must have a no-arg constructor and compatible type.
*
*
* The default value of this property is {@link Converter}, which means do not set a specific
* {@link Converter} on the field.
*
* @return field value converter
* @see com.vaadin.ui.AbstractField#setConverter(Converter)
*/
@SuppressWarnings("rawtypes")
Class extends Converter> converter() default Converter.class;
/**
* Get {@link Validator} types to add to this field. All such types must have no-arg constructors.
*
* @return field validators
* @see com.vaadin.ui.AbstractField#addValidator
*/
Class extends Validator>[] validators() default {};
/**
* Get whether this field is buffered.
*
* @return true for buffered mode
* @see com.vaadin.ui.AbstractField#setBuffered
*/
boolean buffered() default false;
/**
* Get whether invalid values are allowed.
*
* @return true to allow invalid values
* @see com.vaadin.ui.AbstractField#setInvalidAllowed
*/
boolean invalidAllowed() default true;
/**
* Get whether invalid values should be committed.
*
* @return true to allow invalid values to be committed
* @see com.vaadin.ui.AbstractField#setInvalidCommitted
*/
boolean invalidCommitted() default false;
/**
* Get error message when value cannot be converted to data model type.
*
* @return error message
* @see com.vaadin.ui.AbstractField#setConversionError
*/
String conversionError() default "Could not convert value to {0}";
/**
* Get the error that is shown if this field is required, but empty.
*
* @return error message
* @see com.vaadin.ui.AbstractField#setRequiredError
*/
String requiredError() default "";
/**
* Get whether automatic visible validation is enabled.
*
* @return true for automatic visible validation
* @see com.vaadin.ui.AbstractField#setValidationVisible
*/
boolean validationVisible() default true;
/**
* Get whether field is required.
*
* @return true if value is required
* @see com.vaadin.ui.AbstractField#setRequired
*/
boolean required() default false;
/**
* Get tabular index.
*
* @return field tab index
* @see com.vaadin.ui.AbstractField#setTabIndex
*/
int tabIndex() default 0;
}
/**
* Specifies how a Java property should be edited in Vaadin using an {@link com.vaadin.ui.AbstractSelect}.
*
* @see FieldBuilder.AbstractField
* @see FieldBuilder
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface AbstractSelect {
/**
* Get the {@link com.vaadin.ui.AbstractSelect} type that will edit the property.
*
*
* Although this property has a default value, it must be overridden either in this annotation, or
* by also including a more specific annotation such as {@link ComboBox}.
*
* @return field type
*/
Class extends com.vaadin.ui.AbstractSelect> type() default com.vaadin.ui.AbstractSelect.class;
/**
* Get the item caption mode.
*
* @return caption mode
* @see com.vaadin.ui.AbstractSelect#setItemCaptionMode
*/
com.vaadin.ui.AbstractSelect.ItemCaptionMode itemCaptionMode()
default com.vaadin.ui.AbstractSelect.ItemCaptionMode.EXPLICIT_DEFAULTS_ID;
/**
* Get the item caption property ID (which must be a string).
*
* @return caption property ID
* @see com.vaadin.ui.AbstractSelect#setItemCaptionPropertyId
*/
String itemCaptionPropertyId() default "";
/**
* Get the item icon property ID (which must be a string).
*
* @return icon property ID
* @see com.vaadin.ui.AbstractSelect#setItemIconPropertyId
*/
String itemIconPropertyId() default "";
/**
* Get the null selection item ID.
*
* @return null selection item ID
* @see com.vaadin.ui.AbstractSelect#setNullSelectionItemId
*/
String nullSelectionItemId() default "";
/**
* Get multi-select setting.
*
* @return true to allow multiple select
* @see com.vaadin.ui.AbstractSelect#setMultiSelect
*/
boolean multiSelect() default false;
/**
* Get whether new items are allowed.
*
* @return true to allow new items
* @see com.vaadin.ui.AbstractSelect#setNewItemsAllowed
*/
boolean newItemsAllowed() default false;
/**
* Get whether null selection is allowed.
*
* @return true to allow null selection
* @see com.vaadin.ui.AbstractSelect#setNullSelectionAllowed
*/
boolean nullSelectionAllowed() default true;
}
/**
* Specifies how a Java property should be edited in Vaadin using an {@link com.vaadin.ui.CheckBox}.
*
* @see FieldBuilder.AbstractField
* @see FieldBuilder
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface CheckBox {
/**
* Get the {@link com.vaadin.ui.CheckBox} type that will edit the property. Type must have a no-arg constructor.
*
* @return field type
*/
Class extends com.vaadin.ui.CheckBox> type() default com.vaadin.ui.CheckBox.class;
}
/**
* Specifies how a Java property should be edited in Vaadin using an {@link com.vaadin.ui.ComboBox}.
*
* @see FieldBuilder.AbstractField
* @see FieldBuilder.AbstractSelect
* @see FieldBuilder
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface ComboBox {
/**
* Get the {@link com.vaadin.ui.ComboBox} type that will edit the property. Type must have a no-arg constructor.
*
* @return field type
*/
Class extends com.vaadin.ui.ComboBox> type() default com.vaadin.ui.ComboBox.class;
/**
* Get the input prompt.
*
* @return input prompt
* @see com.vaadin.ui.ComboBox#setInputPrompt
*/
String inputPrompt() default "";
/**
* Get the page length.
*
* @return page length, or -1 for none
* @see com.vaadin.ui.ComboBox#setPageLength
*/
int pageLength() default -1;
/**
* Get whether to scroll to the selected item.
*
* @return true to scroll to the selected item
* @see com.vaadin.ui.ComboBox#setScrollToSelectedItem
*/
boolean scrollToSelectedItem() default true;
/**
* Get whether text input is allowed.
*
* @return true to allow text input
* @see com.vaadin.ui.ComboBox#setTextInputAllowed
*/
boolean textInputAllowed() default true;
/**
* Get the filtering mode.
*
* @return filtering mode
* @see com.vaadin.ui.ComboBox#setFilteringMode
*/
FilteringMode filteringMode() default FilteringMode.STARTSWITH;
}
/**
* Specifies how a Java property should be edited in Vaadin using an {@link org.dellroad.stuff.vaadin7.EnumComboBox}.
*
* @see FieldBuilder.AbstractField
* @see FieldBuilder.AbstractSelect
* @see FieldBuilder.ComboBox
* @see FieldBuilder
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface EnumComboBox {
/**
* Get the {@link org.dellroad.stuff.vaadin7.EnumComboBox} type that will edit the property.
* Type must have a no-arg constructor.
*
* @return field type
*/
Class extends org.dellroad.stuff.vaadin7.EnumComboBox> type() default org.dellroad.stuff.vaadin7.EnumComboBox.class;
/**
* Get the {@link Enum} type to choose from. If left as default, the type will be inferred from
* the getter method return type, which must be an {@link Enum} type.
*
* @return enum type
* @see org.dellroad.stuff.vaadin7.EnumComboBox#setEnumDataSource
*/
@SuppressWarnings("rawtypes")
Class extends Enum> enumClass() default Enum.class;
}
/**
* Specifies how a Java property should be edited in Vaadin using an {@link com.vaadin.ui.ListSelect}.
*
* @see FieldBuilder.AbstractField
* @see FieldBuilder.AbstractSelect
* @see FieldBuilder
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface ListSelect {
/**
* Get the {@link com.vaadin.ui.ListSelect} type that will edit the property. Type must have a no-arg constructor.
*
* @return field type
*/
Class extends com.vaadin.ui.ListSelect> type() default com.vaadin.ui.ListSelect.class;
/**
* Get the number of rows in the editor.
*
* @return number of rows, or -1 for none
* @see com.vaadin.ui.ListSelect#setRows
*/
int rows() default -1;
}
/**
* Specifies how a Java property should be edited in Vaadin using an {@link com.vaadin.ui.DateField}.
*
* @see FieldBuilder.AbstractField
* @see FieldBuilder
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface DateField {
/**
* Get the {@link com.vaadin.ui.DateField} type that will edit the property. Type must have a no-arg constructor.
*
* @return field type
*/
Class extends com.vaadin.ui.DateField> type() default com.vaadin.ui.DateField.class;
/**
* Get the date format string.
*
* @return date format string
* @see com.vaadin.ui.DateField#setDateFormat
*/
String dateFormat() default "";
/**
* Get the date parse error message.
*
* @return date parse error message
* @see com.vaadin.ui.DateField#setParseErrorMessage
*/
String parseErrorMessage() default "";
/**
* Get the date out of range error message.
*
* @return date out of range error message
* @see com.vaadin.ui.DateField#setDateOutOfRangeMessage
*/
String dateOutOfRangeMessage() default "";
/**
* Get the date resolution.
*
* @return date resolution
* @see com.vaadin.ui.DateField#setResolution
*/
Resolution resolution() default Resolution.DAY;
/**
* Get whether to show ISO week numbers.
*
* @return whether to show ISO week numbers
* @see com.vaadin.ui.DateField#setShowISOWeekNumbers
*/
boolean showISOWeekNumbers() default false;
/**
* Get the time zone (in string form).
*
* @return time zone name
* @see com.vaadin.ui.DateField#setTimeZone
*/
String timeZone() default "";
/**
* Get lenient mode.
*
* @return true for lenient mode
* @see com.vaadin.ui.DateField#setLenient
*/
boolean lenient() default false;
}
/**
* Specifies how a Java property should be edited in Vaadin using a {@link com.vaadin.ui.AbstractTextField}.
*
* @see FieldBuilder.AbstractField
* @see FieldBuilder
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface AbstractTextField {
/**
* Get the {@link AbstractTextField} type that will edit the property.
*
* @return field type
*/
Class extends com.vaadin.ui.AbstractTextField> type() default com.vaadin.ui.TextField.class;
/**
* Get the representation of null.
*
* @return representation for null value
* @see com.vaadin.ui.AbstractTextField#setNullRepresentation
*/
String nullRepresentation() default "null";
/**
* Get whether null value may be set.
*
* @return whether null value is allowed
* @see com.vaadin.ui.AbstractTextField#setNullSettingAllowed
*/
boolean nullSettingAllowed() default false;
/**
* Get text change event mode.
*
* @return text change event mode
* @see com.vaadin.ui.AbstractTextField#setTextChangeEventMode
*/
com.vaadin.ui.AbstractTextField.TextChangeEventMode textChangeEventMode()
default com.vaadin.ui.AbstractTextField.TextChangeEventMode.LAZY;
/**
* Get text change event timeout.
*
* @return text change event timeout in seconds, or -1 to not override
* @see com.vaadin.ui.AbstractTextField#setTextChangeTimeout
*/
int textChangeTimeout() default -1;
/**
* Get the input prompt.
*
* @return input prompt
* @see com.vaadin.ui.AbstractTextField#setInputPrompt
*/
String inputPrompt() default "";
/**
* Get the number of columns.
*
* @return number of columns, or zero to not override
* @see com.vaadin.ui.AbstractTextField#setColumns
*/
int columns() default 0;
/**
* Get the maximum length.
*
* @return maximum length, or -1 to not override
* @see com.vaadin.ui.AbstractTextField#setMaxLength
*/
int maxLength() default -1;
}
/**
* Specifies how a Java property should be edited in Vaadin using a {@link com.vaadin.ui.TextField}.
*
* @see FieldBuilder.AbstractField
* @see FieldBuilder.AbstractTextField
* @see FieldBuilder
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface TextField {
/**
* Get the {@link TextField} type that will edit the property.
*
* @return field type
*/
Class extends com.vaadin.ui.TextField> type() default com.vaadin.ui.TextField.class;
}
/**
* Specifies how a Java property should be edited in Vaadin using a {@link com.vaadin.ui.TextArea}.
*
* @see FieldBuilder.AbstractField
* @see FieldBuilder.AbstractTextField
* @see FieldBuilder
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface TextArea {
/**
* Get the {@link TextArea} type that will edit the property.
*
* @return field type
*/
Class extends com.vaadin.ui.TextArea> type() default com.vaadin.ui.TextArea.class;
/**
* Set the number of rows.
*
* @return number of rows, or -1 for none
* @see com.vaadin.ui.TextArea#setRows
*/
int rows() default -1;
/**
* Set wordwrap mode.
*
* @return word wrap mode
* @see com.vaadin.ui.TextArea#setWordwrap
*/
boolean wordwrap() default true;
}
/**
* Specifies how a Java property should be edited in Vaadin using a {@link com.vaadin.ui.PasswordField}.
*
* @see FieldBuilder.AbstractField
* @see FieldBuilder.AbstractTextField
* @see FieldBuilder
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface PasswordField {
/**
* Get the {@link PasswordField} type that will edit the property.
*
* @return field type
*/
Class extends com.vaadin.ui.PasswordField> type() default com.vaadin.ui.PasswordField.class;
}
}