
org.dellroad.stuff.vaadin24.field.AbstractFieldBuilder Maven / Gradle / Ivy
Show all versions of dellroad-stuff-vaadin24 Show documentation
/*
* Copyright (C) 2022 Archie L. Cobbs. All rights reserved.
*/
package org.dellroad.stuff.vaadin24.field;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.HasEnabled;
import com.vaadin.flow.component.HasStyle;
import com.vaadin.flow.component.HasValue;
import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.binder.BindingValidationStatusHandler;
import com.vaadin.flow.data.binder.ErrorMessageProvider;
import com.vaadin.flow.data.binder.Validator;
import com.vaadin.flow.data.converter.Converter;
import com.vaadin.flow.dom.Style;
import com.vaadin.flow.shared.util.SharedUtil;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
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.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.dellroad.stuff.java.AnnotationUtil;
import org.dellroad.stuff.java.MethodAnnotationScanner;
import org.dellroad.stuff.java.Primitive;
import org.dellroad.stuff.java.ReflectUtil;
/**
* Provides the machinery for auto-generated {@link FieldBuilder}-like classes.
*
*
* The annotation classes defined in this class, which are common to all {@link FieldBuilder}-like classes, may
* be referenced using the concrete subclass name for consistency. For example, code can reference
* {@link Binding @FieldBuilder.Binding} instead of {@link Binding @AbstractFieldBuilder.Binding}.
*
*
* See {@link FieldBuilder} for details and the standard implementation.
*
* @param subclass type
* @param edited model type
* @see FieldBuilder
*/
public abstract class AbstractFieldBuilder, T> implements Serializable {
public static final String DEFAULT_IMPLEMENTATION_PROPERTY_NAME = "implementation";
public static final String DEFAULT_ANNOTATION_DEFAULTS_METHOD_NAME = "annotationDefaultsMethod";
private static final String STRING_DEFAULT = "";
private static final long serialVersionUID = -3091638771700394722L;
// Static info
private final Class type;
private transient LinkedHashMap bindingInfoMap; // info from scanned annotations
private transient HashMap, Map> defaultInfoMap; // info from scanned @FieldDefault's
// Mutable info
private LinkedHashMap> fieldComponentMap; // fields most recently built by bindFields()
// Constructors
/**
* Constructor.
*
* @param type backing object type
* @throws IllegalArgumentException if {@code type} is null
*/
protected AbstractFieldBuilder(Class type) {
if (type == null)
throw new IllegalArgumentException("null type");
this.type = type;
this.scanForAnnotations();
}
/**
* Static information copy constructor.
*
*
* Using this constructor is more efficient than repeatedly scanning the same classes for the same annotations.
*
*
* Only the static information gathered by this instance by scanning for annotations is copied.
* Any previously built fields are not copied.
*
* @param original original instance
* @throws IllegalArgumentException if {@code original} is null
*/
protected AbstractFieldBuilder(AbstractFieldBuilder original) {
if (original == null)
throw new IllegalArgumentException("null original");
this.type = original.type;
this.bindingInfoMap = new LinkedHashMap<>(original.bindingInfoMap);
this.defaultInfoMap = new HashMap<>(original.defaultInfoMap);
}
// Methods
/**
* Get the type associated with this instance.
*
* @return configured type
*/
public Class getType() {
return this.type;
}
/**
* Get all of the properties discovered by this instance from scanned annotations.
*
*
* This represents static information gathered by this instance by scanning the class hierarchy during construction.
*
*
* The returned {@link Map} iterates in order of {@link FormLayout#order @FieldBuilder.FormLayout.order()},
* then by property name.
*
* @return unmodifiable mapping of scanned properties keyed by property name
*/
public Map getScannedProperties() {
return Collections.unmodifiableMap(this.bindingInfoMap);
}
/**
* Get the default values discovered by this instance (if any) from scanned
* {@link AbstractFieldBuilder.FieldDefault @FieldBuilder.FieldDefault} annotations.
*
*
* The returned map is keyed by the return types of methods found with
* {@link FieldBuilder @FieldBuilder.Foo} declarative annotations.
*
*
* This represents static information gathered by this instance by scanning the class hierarchy during construction.
*
* @return unmodifiable mapping from model class to field defaults keyed by field property name
*/
public Map, Map> getScannedFieldDefaults() {
return Collections.unmodifiableMap(this.defaultInfoMap);
}
/**
* Create, configure, and bind fields into the given {@link Binder}.
*
*
* If the {@code binder} does not have a bean currently bound to it, then any
* {@link ProvidesField @FieldBuilder.ProvidesField} annotations on instance methods will generate an error.
*
*
* After this method completes, the associated components can be obtained via {@link #getFieldComponents getFieldComponents()}
* or added to a {@link com.vaadin.flow.component.formlayout.FormLayout} via {@link #addFieldComponents addFieldComponents()}.
*
* @param binder target binder
* @throws IllegalArgumentException if invalid annotation use is encountered
* @throws IllegalArgumentException if {@code binder} is null
*/
public void bindFields(Binder extends T> binder) {
// Sanity check
if (binder == null)
throw new IllegalArgumentException("null binder");
// Create and bind new fields and save associated components
this.fieldComponentMap = new LinkedHashMap<>();
this.bindingInfoMap.forEach((propertyName, info) -> {
final FieldComponent> fieldComponent = info.createFieldComponent(binder.getBean());
info.bind(binder, fieldComponent.getField());
this.fieldComponentMap.put(propertyName, fieldComponent);
});
// Add bean-level validation from ValidatingBean, if appropriate
if (ValidatingBean.class.isAssignableFrom(this.type))
binder.withValidator((value, context) -> ((ValidatingBean)value).validateBean(context));
// Process @EnabledBy dependencies
this.bindingInfoMap.forEach((name, info) -> this.configureEnabledBy(binder, name, info));
}
/**
* Configure the given target field to be automatically enabled/disabled based on the value of the given controlling field.
*/
private void configureEnabledBy(Binder extends T> binder, String targetFieldName, BindingInfo targetFieldInfo) {
// Get @EnabledBy annotation, if any
final EnabledBy enabledBy = targetFieldInfo.getEnabledBy();
if (enabledBy == null)
return;
// Gather target field info
final boolean requireAll = enabledBy.requireAll();
final boolean resetOnDisable = enabledBy.resetOnDisable();
final HasValue, ?> targetField0 = this.fieldComponentMap.get(targetFieldName).getField();
final HasEnabled targetField;
try {
targetField = (HasEnabled)targetField0;
} catch (ClassCastException e) {
throw new IllegalArgumentException(String.format(
"field \"%s\" has @EnabledBy annotation but its type %s does not implement %s",
targetFieldName, targetField0.getClass().getName(), HasEnabled.class.getName()), e);
}
// Gather controlling fields names
final String[] controllingFieldNames = enabledBy.value();
if (controllingFieldNames.length == 0)
return;
// This is the information we need for each controlling field
class ControllingField {
final HasValue, ?> field;
final String nullRepresentation;
final AtomicReference