
org.dellroad.stuff.vaadin24.data.SimplePropertySet 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.data;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.binder.PropertyDefinition;
import com.vaadin.flow.data.binder.PropertySet;
import com.vaadin.flow.data.binder.Setter;
import com.vaadin.flow.function.ValueProvider;
import java.beans.IndexedPropertyDescriptor;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import org.dellroad.stuff.java.Primitive;
import org.dellroad.stuff.java.ReflectUtil;
/**
* Straightforward implementation of {@link PropertySet} using caller-supplied getters and setters.
*
*
*
*
*
*
* This class is useful for building arbitrary property sets, e.g., see {@link MapPropertySet}.
*
*
* It's also useful when you need to detect Java bean properties defined by default interface methods.
* Due to JDK-8071693, Vaadin's {@link Binder}
* fails to detect such bean properties. To work around that bug, you can do something like this:
*
*
* // Gather bean properties using Spring's BeanUtils to work around JDK-8071693
* final SimplePropertySet<T> propertySet = new SimplePropertySet<>(beanType);
* Stream.of(BeanUtils.getPropertyDescriptors(beanType))
* .filter(pd -> !(pd instanceof IndexedPropertyDescriptor))
* .filter(pd -> pd.getReadMethod() != null)
* .filter(pd -> pd.getWriteMethod() != null)
* .forEach(propertySet::addPropertyDefinition);
*
* // Create binder
* final Binder<T> binder = Binder.withPropertySet(propertySet);
*
*
*
* This class allows you to recover the original {@linkplain Definition property definition} from
* a {@link Binder.Binding} instance; see {@link #propertyDefinitionForBinding propertyDefinitionForBinding()}.
*
*
* Does not support sub-properties.
*
* @param underlying target type
* @see JDK-8071693
*/
@SuppressWarnings("serial")
public class SimplePropertySet implements PropertySet {
private final Map> propertyMap = new LinkedHashMap<>();
private final Class targetType;
/**
* Constructor.
*
* @param targetType object type that contains property values
* @throws IllegalArgumentException if {@code targetType} is null
*/
public SimplePropertySet(Class targetType) {
if (targetType == null)
throw new IllegalArgumentException("null targetType");
this.targetType = targetType;
}
// PropertySet
@Override
public Stream> getProperties() {
return this.propertyMap.values().stream().map(x -> x);
}
@Override
public Optional> getProperty(String name) {
return Optional.ofNullable(this.propertyMap.get(name));
}
// Methods
/**
* Get the target object type associated with this instance.
*
*
* The target object stores the actual property values.
*
* @return target object type
*/
public Class getTargetType() {
return this.targetType;
}
/**
* Add a new property to this instance.
*
* @param name property name
* @param type property type
* @param caption property caption
* @param getter getter method
* @param setter setter method, or null for none
* @return newly created property definition
* @throws IllegalArgumentException if any parameter other than {@code setter} is null
* @throws IllegalArgumentException if a property with the same name has already been added
*/
public Definition addPropertyDefinition(String name, Class type,
String caption, ValueProvider super T, ? extends V> getter, Setter super T, ? super V> setter) {
final Definition newDefinition = new Definition(name, type, caption, getter, setter);
final Definition> oldDefinition = this.propertyMap.putIfAbsent(name, newDefinition);
if (oldDefinition != null)
throw new IllegalArgumentException("duplicate name");
return newDefinition;
}
/**
* Add a new property to this instance corresponding to the given Java bean {@link PropertyDescriptor}.
*
*
* The caller is responsible for ensuring that {@code propertyDescriptor} is compatible with the target object type.
*
* @param propertyDescriptor property descriptor
* @return newly created property definition
* @throws IllegalArgumentException if {@code propertyDescriptor} is null
* @throws IllegalArgumentException if {@code propertyDescriptor} is an {@link IndexedPropertyDescriptor}
* @throws IllegalArgumentException if {@code propertyDescriptor} has no getter method
* @throws IllegalArgumentException if a property with the same name has already been added
*/
public Definition> addPropertyDefinition(PropertyDescriptor propertyDescriptor) {
if (propertyDescriptor == null)
throw new IllegalArgumentException("null propertyDescriptor");
if (propertyDescriptor instanceof IndexedPropertyDescriptor)
throw new IllegalArgumentException(IndexedPropertyDescriptor.class + " unsupported");
return this.addPropertyDefinition(propertyDescriptor.getName(), propertyDescriptor.getPropertyType(),
propertyDescriptor.getDisplayName(), propertyDescriptor.getReadMethod(), propertyDescriptor.getWriteMethod());
}
// This method exists solely to bind the generic type
private Definition addPropertyDefinition(String name, Class type,
String caption, Method getter, Method setter) {
if (getter == null)
throw new IllegalArgumentException("null getter");
return this.addPropertyDefinition(name, type, caption,
target -> Primitive.wrap(type).cast(ReflectUtil.invoke(getter, target)),
setter != null ? (target, value) -> ReflectUtil.invoke(setter, target, value) : null);
}
/**
* Recover the {@link SimplePropertySet.Definition} from the given binding, assuming the associated {@link Binder}
* was created using a {@link SimplePropertySet}.
*
* @param binding {@link Binder} binding
* @return binding's associated property definition
* @throws IllegalArgumentException if the associated {@link Binder} does not use a {@link SimplePropertySet}
* @throws IllegalArgumentException if {@code binding} is null
*/
public static SimplePropertySet>.Definition> propertyDefinitionForBinding(Binder.Binding, ?> binding) {
if (binding == null)
throw new IllegalArgumentException("null binding");
return Optional.ofNullable(binding.getGetter())
.filter(SimplePropertySet.Definition.Getter.class::isInstance)
.map(SimplePropertySet.Definition.Getter.class::cast)
.map(SimplePropertySet>.Definition>.Getter::getDefinition)
.orElseThrow(() -> new IllegalArgumentException("binding's binder does not use SimplePropertySet"));
}
/**
* Create a new {@link Definition}.
*
*
* The implementation in {@link SimplePropertySet} just invokes the {@link Definition} constructor directly.
*
* @param name property name
* @param type property type
* @param caption property caption
* @param getter getter method
* @param setter setter method, or null for none
* @return newly created property definition
* @throws IllegalArgumentException if any parameter other than {@code setter} is null
*/
protected Definition createDefinition(String name, Class type,
String caption, ValueProvider super T, ? extends V> getter, Setter super T, ? super V> setter) {
return new Definition(name, type, caption, getter, setter);
}
// Definition
/**
* A {@link PropertyDefinition} within a {@link SimplePropertySet}.
*
*
* Instances provide {@link Getter}s that allow recovery of the originating instance; see
* {@link SimplePropertySet#propertyDefinitionForBinding SimplePropertySet.propertyDefinitionForBinding()}.
*
* @param property value type
*/
@SuppressWarnings("serial")
public class Definition implements PropertyDefinition {
private final String name;
private final Class type;
private final String caption;
private final ValueProvider super T, ? extends V> getter;
private final Setter super T, ? super V> setter;
/**
* Constructor.
*
* @param name property name
* @param type property type
* @param caption property caption
* @param getter getter method
* @param setter setter method, or null for none
* @throws IllegalArgumentException if any parameter other than {@code setter} is null
*/
public Definition(String name, Class type, String caption,
ValueProvider super T, ? extends V> getter, Setter super T, ? super V> setter) {
if (name == null)
throw new IllegalArgumentException("null name");
if (type == null)
throw new IllegalArgumentException("null type");
if (caption == null)
throw new IllegalArgumentException("null caption");
if (getter == null)
throw new IllegalArgumentException("null getter");
this.name = name;
this.type = type;
this.caption = caption;
this.getter = getter;
this.setter = setter;
}
@Override
public String getCaption() {
return this.caption;
}
@Override
public Getter getGetter() {
return new Getter();
}
@Override
public String getName() {
return this.name;
}
@Override
public PropertyDefinition getParent() {
return null;
}
@Override
public Class getPropertyHolderType() {
return SimplePropertySet.this.targetType;
}
@Override
public PropertySet getPropertySet() {
return SimplePropertySet.this;
}
@Override
public Optional> getSetter() {
return Optional.ofNullable(this.setter).map(s -> s::accept);
}
@Override
public Class getType() {
return Primitive.wrap(this.type); // see https://github.com/vaadin/flow/issues/13992
}
@Override
public boolean isGenericType() {
return this.type.getTypeParameters().length > 0;
}
// Getter
// We use a wrapper class here so propertyDefinitionForBinding() can work
@SuppressWarnings("serial")
public class Getter implements ValueProvider {
public Definition getDefinition() {
return Definition.this;
}
@Override
public V apply(T target) {
if (target == null)
throw new IllegalArgumentException("null target");
return Definition.this.getter.apply(target);
}
}
}
}