All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dellroad.stuff.vaadin24.data.SimplePropertySet Maven / Gradle / Ivy

The newest version!

/*
 * 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 getter, Setter 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 getter, Setter 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 getter; private final Setter 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 getter, Setter 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); } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy