You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

BeanBinder.java 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. /*
  2. * Copyright 2000-2016 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.data;
  17. import java.beans.IntrospectionException;
  18. import java.beans.PropertyDescriptor;
  19. import java.lang.reflect.Field;
  20. import java.lang.reflect.InvocationTargetException;
  21. import java.lang.reflect.Method;
  22. import java.lang.reflect.Type;
  23. import java.util.ArrayList;
  24. import java.util.HashSet;
  25. import java.util.List;
  26. import java.util.Locale;
  27. import java.util.Objects;
  28. import java.util.Optional;
  29. import java.util.Set;
  30. import java.util.function.BiConsumer;
  31. import com.googlecode.gentyref.GenericTypeReflector;
  32. import com.vaadin.annotations.PropertyId;
  33. import com.vaadin.data.util.BeanUtil;
  34. import com.vaadin.data.util.converter.Converter;
  35. import com.vaadin.data.validator.BeanValidator;
  36. import com.vaadin.server.SerializableBiConsumer;
  37. import com.vaadin.server.SerializableFunction;
  38. import com.vaadin.server.SerializablePredicate;
  39. import com.vaadin.util.ReflectTools;
  40. /**
  41. * A {@code Binder} subclass specialized for binding <em>beans</em>: classes
  42. * that conform to the JavaBeans specification. Bean properties are bound by
  43. * their names. If a JSR-303 bean validation implementation is present on the
  44. * classpath, {@code BeanBinder} adds a {@link BeanValidator} to each binding.
  45. *
  46. * @author Vaadin Ltd.
  47. *
  48. * @param <BEAN>
  49. * the bean type
  50. *
  51. * @since 8.0
  52. */
  53. public class BeanBinder<BEAN> extends Binder<BEAN> {
  54. /**
  55. * Represents the binding between a single field and a bean property.
  56. *
  57. * @param <BEAN>
  58. * the bean type
  59. * @param <FIELDVALUE>
  60. * the field value type
  61. * @param <TARGET>
  62. * the target property type
  63. */
  64. public interface BeanBinding<BEAN, FIELDVALUE, TARGET>
  65. extends Binding<BEAN, FIELDVALUE, TARGET> {
  66. @Override
  67. public BeanBinding<BEAN, FIELDVALUE, TARGET> withValidator(
  68. Validator<? super TARGET> validator);
  69. @Override
  70. public default BeanBinding<BEAN, FIELDVALUE, TARGET> withValidator(
  71. SerializablePredicate<? super TARGET> predicate,
  72. String message) {
  73. return (BeanBinding<BEAN, FIELDVALUE, TARGET>) Binding.super.withValidator(
  74. predicate, message);
  75. }
  76. @Override
  77. public <NEWTARGET> BeanBinding<BEAN, FIELDVALUE, NEWTARGET> withConverter(
  78. Converter<TARGET, NEWTARGET> converter);
  79. @Override
  80. public default <NEWTARGET> BeanBinding<BEAN, FIELDVALUE, NEWTARGET> withConverter(
  81. SerializableFunction<TARGET, NEWTARGET> toModel,
  82. SerializableFunction<NEWTARGET, TARGET> toPresentation) {
  83. return (BeanBinding<BEAN, FIELDVALUE, NEWTARGET>) Binding.super.withConverter(
  84. toModel, toPresentation);
  85. }
  86. @Override
  87. public default <NEWTARGET> BeanBinding<BEAN, FIELDVALUE, NEWTARGET> withConverter(
  88. SerializableFunction<TARGET, NEWTARGET> toModel,
  89. SerializableFunction<NEWTARGET, TARGET> toPresentation,
  90. String errorMessage) {
  91. return (BeanBinding<BEAN, FIELDVALUE, NEWTARGET>) Binding.super.withConverter(
  92. toModel, toPresentation, errorMessage);
  93. }
  94. /**
  95. * Completes this binding by connecting the field to the property with
  96. * the given name. The getter and setter methods of the property are
  97. * looked up with bean introspection and used to read and write the
  98. * property value.
  99. * <p>
  100. * If a JSR-303 bean validation implementation is present on the
  101. * classpath, adds a {@link BeanValidator} to this binding.
  102. * <p>
  103. * The property must have an accessible getter method. It need not have
  104. * an accessible setter; in that case the property value is never
  105. * updated and the binding is said to be <i>read-only</i>.
  106. *
  107. * @param propertyName
  108. * the name of the property to bind, not null
  109. *
  110. * @throws IllegalArgumentException
  111. * if the property name is invalid
  112. * @throws IllegalArgumentException
  113. * if the property has no accessible getter
  114. *
  115. * @see Binding#bind(SerializableFunction, SerializableBiConsumer)
  116. */
  117. public void bind(String propertyName);
  118. }
  119. /**
  120. * An internal implementation of {@link BeanBinding}.
  121. *
  122. * @param <BEAN>
  123. * the bean type
  124. * @param <FIELDVALUE>
  125. * the field value type
  126. * @param <TARGET>
  127. * the target property type
  128. */
  129. protected static class BeanBindingImpl<BEAN, FIELDVALUE, TARGET>
  130. extends BindingImpl<BEAN, FIELDVALUE, TARGET>
  131. implements BeanBinding<BEAN, FIELDVALUE, TARGET> {
  132. private Method getter;
  133. private Method setter;
  134. /**
  135. * Creates a new bean binding.
  136. *
  137. * @param binder
  138. * the binder this instance is connected to, not null
  139. * @param field
  140. * the field to use, not null
  141. * @param converter
  142. * the initial converter to use, not null
  143. * @param statusHandler
  144. * the handler to notify of status changes, not null
  145. */
  146. protected BeanBindingImpl(BeanBinder<BEAN> binder,
  147. HasValue<FIELDVALUE> field,
  148. Converter<FIELDVALUE, TARGET> converter,
  149. ValidationStatusHandler statusHandler) {
  150. super(binder, field, converter, statusHandler);
  151. }
  152. @Override
  153. public BeanBinding<BEAN, FIELDVALUE, TARGET> withValidator(
  154. Validator<? super TARGET> validator) {
  155. return (BeanBinding<BEAN, FIELDVALUE, TARGET>) super.withValidator(
  156. validator);
  157. }
  158. @Override
  159. public <NEWTARGET> BeanBinding<BEAN, FIELDVALUE, NEWTARGET> withConverter(
  160. Converter<TARGET, NEWTARGET> converter) {
  161. return (BeanBinding<BEAN, FIELDVALUE, NEWTARGET>) super.withConverter(
  162. converter);
  163. }
  164. @Override
  165. public void bind(String propertyName) {
  166. checkUnbound();
  167. Binding<BEAN, FIELDVALUE, Object> finalBinding;
  168. finalBinding = withConverter(createConverter());
  169. if (BeanUtil.checkBeanValidationAvailable()) {
  170. finalBinding = finalBinding.withValidator(new BeanValidator(
  171. getBinder().beanType, propertyName, findLocale()));
  172. }
  173. PropertyDescriptor descriptor = getDescriptor(propertyName);
  174. getter = descriptor.getReadMethod();
  175. setter = descriptor.getWriteMethod();
  176. finalBinding.bind(this::getValue, this::setValue);
  177. getBinder().boundProperties.add(propertyName);
  178. }
  179. @Override
  180. protected BeanBinder<BEAN> getBinder() {
  181. return (BeanBinder<BEAN>) super.getBinder();
  182. }
  183. private void setValue(BEAN bean, Object value) {
  184. try {
  185. if (setter != null) {
  186. setter.invoke(bean, value);
  187. }
  188. } catch (IllegalAccessException | InvocationTargetException e) {
  189. throw new RuntimeException(e);
  190. }
  191. }
  192. private Object getValue(BEAN bean) {
  193. try {
  194. return getter.invoke(bean);
  195. } catch (IllegalAccessException | InvocationTargetException e) {
  196. throw new RuntimeException(e);
  197. }
  198. }
  199. private PropertyDescriptor getDescriptor(String propertyName) {
  200. final Class<?> beanType = getBinder().beanType;
  201. PropertyDescriptor descriptor = null;
  202. try {
  203. descriptor = BeanUtil.getPropertyDescriptor(beanType,
  204. propertyName);
  205. } catch (IntrospectionException ie) {
  206. throw new IllegalArgumentException(
  207. "Could not resolve bean property name (see the cause): "
  208. + beanType.getName() + "." + propertyName,
  209. ie);
  210. }
  211. if (descriptor == null) {
  212. throw new IllegalArgumentException(
  213. "Could not resolve bean property name (please check spelling and getter visibility): "
  214. + beanType.getName() + "." + propertyName);
  215. }
  216. if (descriptor.getReadMethod() == null) {
  217. throw new IllegalArgumentException(
  218. "Bean property has no accessible getter: "
  219. + beanType.getName() + "." + propertyName);
  220. }
  221. return descriptor;
  222. }
  223. @SuppressWarnings("unchecked")
  224. private Converter<TARGET, Object> createConverter() {
  225. return Converter.from(
  226. fieldValue -> cast(fieldValue, getter.getReturnType()),
  227. propertyValue -> (TARGET) propertyValue, exception -> {
  228. throw new RuntimeException(exception);
  229. });
  230. }
  231. private <T> T cast(TARGET value, Class<T> clazz) {
  232. if (clazz.isPrimitive()) {
  233. return (T) ReflectTools.convertPrimitiveType(clazz).cast(value);
  234. } else {
  235. return clazz.cast(value);
  236. }
  237. }
  238. }
  239. private final Class<? extends BEAN> beanType;
  240. private final Set<String> boundProperties;
  241. /**
  242. * Creates a new {@code BeanBinder} supporting beans of the given type.
  243. *
  244. * @param beanType
  245. * the bean {@code Class} instance, not null
  246. */
  247. public BeanBinder(Class<? extends BEAN> beanType) {
  248. BeanUtil.checkBeanValidationAvailable();
  249. this.beanType = beanType;
  250. boundProperties = new HashSet<>();
  251. }
  252. @Override
  253. public <FIELDVALUE> BeanBinding<BEAN, FIELDVALUE, FIELDVALUE> forField(
  254. HasValue<FIELDVALUE> field) {
  255. return (BeanBinding<BEAN, FIELDVALUE, FIELDVALUE>) super.forField(
  256. field);
  257. }
  258. /**
  259. * Binds the given field to the property with the given name. The getter and
  260. * setter methods of the property are looked up with bean introspection and
  261. * used to read and write the property value.
  262. * <p>
  263. * Use the {@link #forField(HasValue)} overload instead if you want to
  264. * further configure the new binding.
  265. * <p>
  266. * The property must have an accessible getter method. It need not have an
  267. * accessible setter; in that case the property value is never updated and
  268. * the binding is said to be <i>read-only</i>.
  269. *
  270. * @param <FIELDVALUE>
  271. * the value type of the field to bind
  272. * @param field
  273. * the field to bind, not null
  274. * @param propertyName
  275. * the name of the property to bind, not null
  276. *
  277. * @throws IllegalArgumentException
  278. * if the property name is invalid
  279. * @throws IllegalArgumentException
  280. * if the property has no accessible getter
  281. *
  282. * @see #bind(HasValue, SerializableFunction, SerializableBiConsumer)
  283. */
  284. public <FIELDVALUE> void bind(HasValue<FIELDVALUE> field,
  285. String propertyName) {
  286. forField(field).bind(propertyName);
  287. }
  288. @Override
  289. public BeanBinder<BEAN> withValidator(Validator<? super BEAN> validator) {
  290. return (BeanBinder<BEAN>) super.withValidator(validator);
  291. }
  292. @Override
  293. protected <FIELDVALUE, TARGET> BeanBindingImpl<BEAN, FIELDVALUE, TARGET> createBinding(
  294. HasValue<FIELDVALUE> field, Converter<FIELDVALUE, TARGET> converter,
  295. ValidationStatusHandler handler) {
  296. Objects.requireNonNull(field, "field cannot be null");
  297. Objects.requireNonNull(converter, "converter cannot be null");
  298. return new BeanBindingImpl<>(this, field, converter, handler);
  299. }
  300. /**
  301. * Binds member fields found in the given object.
  302. * <p>
  303. * This method processes all (Java) member fields whose type extends
  304. * {@link HasValue} and that can be mapped to a property id. Property id
  305. * mapping is done based on the field name or on a @{@link PropertyId}
  306. * annotation on the field. All non-null unbound fields for which a property
  307. * id can be determined are bound to the property id.
  308. * </p>
  309. * <p>
  310. * For example:
  311. *
  312. * <pre>
  313. * public class MyForm extends VerticalLayout {
  314. * private TextField firstName = new TextField("First name");
  315. * &#64;PropertyId("last")
  316. * private TextField lastName = new TextField("Last name");
  317. *
  318. * MyForm myForm = new MyForm();
  319. * ...
  320. * binder.bindMemberFields(myForm);
  321. * </pre>
  322. *
  323. * </p>
  324. * This binds the firstName TextField to a "firstName" property in the item,
  325. * lastName TextField to a "last" property.
  326. * <p>
  327. * It's not always possible to bind a field to a property because their
  328. * types are incompatible. E.g. custom converter is required to bind
  329. * {@code HasValue<String>} and {@code Integer} property (that would be a
  330. * case of "age" property). In such case {@link IllegalStateException} will
  331. * be thrown unless the field has been configured manually before calling
  332. * the {@link #bindInstanceFields(Object)} method.
  333. * <p>
  334. * It's always possible to do custom binding for any field: the
  335. * {@link #bindInstanceFields(Object)} method doesn't override existing
  336. * bindings.
  337. *
  338. * @param objectWithMemberFields
  339. * The object that contains (Java) member fields to bind
  340. * @throws IllegalStateException
  341. * if there are incompatible HasValue<T> and property types
  342. */
  343. public void bindInstanceFields(Object objectWithMemberFields) {
  344. Class<?> objectClass = objectWithMemberFields.getClass();
  345. getFieldsInDeclareOrder(objectClass).stream()
  346. .filter(memberField -> HasValue.class
  347. .isAssignableFrom(memberField.getType()))
  348. .forEach(memberField -> handleProperty(memberField,
  349. (property, type) -> bindProperty(objectWithMemberFields,
  350. memberField, property, type)));
  351. }
  352. /**
  353. * Binds {@code property} with {@code propertyType} to the field in the
  354. * {@code objectWithMemberFields} instance using {@code memberField} as a
  355. * reference to a member.
  356. *
  357. * @param objectWithMemberFields
  358. * the object that contains (Java) member fields to build and
  359. * bind
  360. * @param memberField
  361. * reference to a member field to bind
  362. * @param property
  363. * property name to bind
  364. * @param propertyType
  365. * type of the property
  366. */
  367. protected void bindProperty(Object objectWithMemberFields,
  368. Field memberField, String property, Class<?> propertyType) {
  369. Type valueType = GenericTypeReflector.getTypeParameter(
  370. memberField.getGenericType(),
  371. HasValue.class.getTypeParameters()[0]);
  372. if (valueType == null) {
  373. throw new IllegalStateException(String.format(
  374. "Unable to detect value type for the member '%s' in the "
  375. + "class '%s'.",
  376. memberField.getName(),
  377. objectWithMemberFields.getClass().getName()));
  378. }
  379. if (propertyType.equals(valueType)) {
  380. HasValue<?> field;
  381. // Get the field from the object
  382. try {
  383. field = (HasValue<?>) ReflectTools.getJavaFieldValue(
  384. objectWithMemberFields, memberField, HasValue.class);
  385. } catch (IllegalArgumentException | IllegalAccessException
  386. | InvocationTargetException e) {
  387. // If we cannot determine the value, just skip the field
  388. return;
  389. }
  390. if (field == null) {
  391. field = makeFieldInstance(
  392. (Class<? extends HasValue<?>>) memberField.getType());
  393. initializeField(objectWithMemberFields, memberField, field);
  394. }
  395. forField(field).bind(property);
  396. } else {
  397. throw new IllegalStateException(String.format(
  398. "Property type '%s' doesn't "
  399. + "match the field type '%s'. "
  400. + "Binding should be configured manulaly using converter.",
  401. propertyType.getName(), valueType.getTypeName()));
  402. }
  403. }
  404. /**
  405. * Makes an instance of the field type {@code fieldClass}.
  406. * <p>
  407. * The resulting field instance is used to bind a property to it using the
  408. * {@link #bindInstanceFields(Object)} method.
  409. * <p>
  410. * The default implementation relies on the default constructor of the
  411. * class. If there is no suitable default constructor or you want to
  412. * configure the instantiated class then override this method and provide
  413. * your own implementation.
  414. *
  415. * @see #bindInstanceFields(Object)
  416. * @param fieldClass
  417. * type of the field
  418. * @return a {@code fieldClass} instance object
  419. */
  420. protected HasValue<?> makeFieldInstance(
  421. Class<? extends HasValue<?>> fieldClass) {
  422. try {
  423. return fieldClass.newInstance();
  424. } catch (InstantiationException | IllegalAccessException e) {
  425. throw new IllegalStateException(
  426. String.format("Couldn't create an '%s' type instance",
  427. fieldClass.getName()),
  428. e);
  429. }
  430. }
  431. /**
  432. * Returns an array containing {@link Field} objects reflecting all the
  433. * fields of the class or interface represented by this Class object. The
  434. * elements in the array returned are sorted in declare order from sub class
  435. * to super class.
  436. *
  437. * @param searchClass
  438. * class to introspect
  439. * @return list of all fields in the class considering hierarchy
  440. */
  441. protected List<Field> getFieldsInDeclareOrder(Class<?> searchClass) {
  442. ArrayList<Field> memberFieldInOrder = new ArrayList<>();
  443. while (searchClass != null) {
  444. for (Field memberField : searchClass.getDeclaredFields()) {
  445. memberFieldInOrder.add(memberField);
  446. }
  447. searchClass = searchClass.getSuperclass();
  448. }
  449. return memberFieldInOrder;
  450. }
  451. private void initializeField(Object objectWithMemberFields,
  452. Field memberField, HasValue<?> value) {
  453. try {
  454. ReflectTools.setJavaFieldValue(objectWithMemberFields, memberField,
  455. value);
  456. } catch (IllegalArgumentException | IllegalAccessException
  457. | InvocationTargetException e) {
  458. throw new IllegalStateException(
  459. String.format("Could not assign value to field '%s'",
  460. memberField.getName()),
  461. e);
  462. }
  463. }
  464. private void handleProperty(Field field,
  465. BiConsumer<String, Class<?>> propertyHandler) {
  466. Optional<PropertyDescriptor> descriptor = getPropertyDescriptor(field);
  467. if (!descriptor.isPresent()) {
  468. return;
  469. }
  470. String propertyName = descriptor.get().getName();
  471. if (boundProperties.contains(propertyName)) {
  472. return;
  473. }
  474. propertyHandler.accept(propertyName,
  475. descriptor.get().getPropertyType());
  476. boundProperties.add(propertyName);
  477. }
  478. private Optional<PropertyDescriptor> getPropertyDescriptor(Field field) {
  479. PropertyId propertyIdAnnotation = field.getAnnotation(PropertyId.class);
  480. String propertyId;
  481. if (propertyIdAnnotation != null) {
  482. // @PropertyId(propertyId) always overrides property id
  483. propertyId = propertyIdAnnotation.value();
  484. } else {
  485. propertyId = field.getName();
  486. }
  487. List<PropertyDescriptor> descriptors;
  488. try {
  489. descriptors = BeanUtil.getBeanPropertyDescriptors(beanType);
  490. } catch (IntrospectionException e) {
  491. throw new IllegalArgumentException(String.format(
  492. "Could not resolve bean '%s' properties (see the cause):",
  493. beanType.getName()), e);
  494. }
  495. Optional<PropertyDescriptor> propertyDescitpor = descriptors.stream()
  496. .filter(descriptor -> minifyFieldName(descriptor.getName())
  497. .equals(minifyFieldName(propertyId)))
  498. .findFirst();
  499. return propertyDescitpor;
  500. }
  501. private String minifyFieldName(String fieldName) {
  502. return fieldName.toLowerCase(Locale.ENGLISH).replace("_", "");
  503. }
  504. }