您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

FieldGroup.java 42KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255
  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.v7.data.fieldgroup;
  17. import java.io.Serializable;
  18. import java.lang.reflect.InvocationTargetException;
  19. import java.util.ArrayList;
  20. import java.util.Collection;
  21. import java.util.Collections;
  22. import java.util.HashMap;
  23. import java.util.LinkedHashMap;
  24. import java.util.List;
  25. import java.util.Map;
  26. import com.vaadin.util.ReflectTools;
  27. import com.vaadin.v7.data.Item;
  28. import com.vaadin.v7.data.Property;
  29. import com.vaadin.v7.data.Validator.InvalidValueException;
  30. import com.vaadin.v7.data.util.TransactionalPropertyWrapper;
  31. import com.vaadin.v7.ui.AbstractField;
  32. import com.vaadin.v7.ui.DefaultFieldFactory;
  33. import com.vaadin.v7.ui.Field;
  34. /**
  35. * FieldGroup provides an easy way of binding fields to data and handling
  36. * commits of these fields.
  37. * <p>
  38. * The typical use case is to create a layout outside the FieldGroup and then
  39. * use FieldGroup to bind the fields to a data source.
  40. * </p>
  41. * <p>
  42. * {@link FieldGroup} is not a UI component so it cannot be added to a layout.
  43. * Using the buildAndBind methods {@link FieldGroup} can create fields for you
  44. * using a FieldGroupFieldFactory but you still have to add them to the correct
  45. * position in your layout.
  46. * </p>
  47. *
  48. * @author Vaadin Ltd
  49. * @since 7.0
  50. */
  51. public class FieldGroup implements Serializable {
  52. private Item itemDataSource;
  53. private boolean buffered = true;
  54. private boolean enabled = true;
  55. private boolean readOnly = false;
  56. private HashMap<Object, Field<?>> propertyIdToField = new HashMap<Object, Field<?>>();
  57. private LinkedHashMap<Field<?>, Object> fieldToPropertyId = new LinkedHashMap<Field<?>, Object>();
  58. private List<CommitHandler> commitHandlers = new ArrayList<CommitHandler>();
  59. /**
  60. * The field factory used by builder methods.
  61. */
  62. private FieldGroupFieldFactory fieldFactory = DefaultFieldGroupFieldFactory
  63. .get();
  64. /**
  65. * Constructs a field binder. Use {@link #setItemDataSource(Item)} to set a
  66. * data source for the field binder.
  67. *
  68. */
  69. public FieldGroup() {
  70. }
  71. /**
  72. * Constructs a field binder that uses the given data source.
  73. *
  74. * @param itemDataSource
  75. * The data source to bind the fields to
  76. */
  77. public FieldGroup(Item itemDataSource) {
  78. setItemDataSource(itemDataSource);
  79. }
  80. /**
  81. * Updates the item that is used by this FieldBinder. Rebinds all fields to
  82. * the properties in the new item.
  83. *
  84. * @param itemDataSource
  85. * The new item to use
  86. */
  87. public void setItemDataSource(Item itemDataSource) {
  88. this.itemDataSource = itemDataSource;
  89. for (Field<?> f : fieldToPropertyId.keySet()) {
  90. bind(f, fieldToPropertyId.get(f));
  91. }
  92. }
  93. /**
  94. * Gets the item used by this FieldBinder. Note that you must call
  95. * {@link #commit()} for the item to be updated unless buffered mode has
  96. * been switched off.
  97. *
  98. * @see #setBuffered(boolean)
  99. * @see #commit()
  100. *
  101. * @return The item used by this FieldBinder
  102. */
  103. public Item getItemDataSource() {
  104. return itemDataSource;
  105. }
  106. /**
  107. * Checks the buffered mode for the bound fields.
  108. * <p>
  109. *
  110. * @see #setBuffered(boolean) for more details on buffered mode
  111. *
  112. * @see Field#isBuffered()
  113. * @return true if buffered mode is on, false otherwise
  114. *
  115. */
  116. public boolean isBuffered() {
  117. return buffered;
  118. }
  119. /**
  120. * Sets the buffered mode for the bound fields.
  121. * <p>
  122. * When buffered mode is on the item will not be updated until
  123. * {@link #commit()} is called. If buffered mode is off the item will be
  124. * updated once the fields are updated.
  125. * </p>
  126. * <p>
  127. * The default is to use buffered mode.
  128. * </p>
  129. *
  130. * @see Field#setBuffered(boolean)
  131. * @param buffered
  132. * true to turn on buffered mode, false otherwise
  133. */
  134. public void setBuffered(boolean buffered) {
  135. if (buffered == this.buffered) {
  136. return;
  137. }
  138. this.buffered = buffered;
  139. for (Field<?> field : getFields()) {
  140. field.setBuffered(buffered);
  141. }
  142. }
  143. /**
  144. * Returns the enabled status for the fields.
  145. * <p>
  146. * Note that this will not accurately represent the enabled status of all
  147. * fields if you change the enabled status of the fields through some other
  148. * method than {@link #setEnabled(boolean)}.
  149. *
  150. * @return true if the fields are enabled, false otherwise
  151. */
  152. public boolean isEnabled() {
  153. return enabled;
  154. }
  155. /**
  156. * Updates the enabled state of all bound fields.
  157. *
  158. * @param fieldsEnabled
  159. * true to enable all bound fields, false to disable them
  160. */
  161. public void setEnabled(boolean fieldsEnabled) {
  162. enabled = fieldsEnabled;
  163. for (Field<?> field : getFields()) {
  164. field.setEnabled(fieldsEnabled);
  165. }
  166. }
  167. /**
  168. * Returns the read only status that is used by default with all fields that
  169. * have a writable data source.
  170. * <p>
  171. * Note that this will not accurately represent the read only status of all
  172. * fields if you change the read only status of the fields through some
  173. * other method than {@link #setReadOnly(boolean)}.
  174. *
  175. * @return true if the fields are set to read only, false otherwise
  176. */
  177. public boolean isReadOnly() {
  178. return readOnly;
  179. }
  180. /**
  181. * Sets the read only state to the given value for all fields with writable
  182. * data source. Fields with read only data source will always be set to read
  183. * only.
  184. *
  185. * @param fieldsReadOnly
  186. * true to set the fields with writable data source to read only,
  187. * false to set them to read write
  188. */
  189. public void setReadOnly(boolean fieldsReadOnly) {
  190. readOnly = fieldsReadOnly;
  191. for (Field<?> field : getFields()) {
  192. if (field.getPropertyDataSource() == null
  193. || !field.getPropertyDataSource().isReadOnly()) {
  194. field.setReadOnly(fieldsReadOnly);
  195. } else {
  196. field.setReadOnly(true);
  197. }
  198. }
  199. }
  200. /**
  201. * Returns a collection of all fields that have been bound.
  202. * <p>
  203. * The fields are not returned in any specific order.
  204. * </p>
  205. *
  206. * @return A collection with all bound Fields
  207. */
  208. public Collection<Field<?>> getFields() {
  209. return fieldToPropertyId.keySet();
  210. }
  211. /**
  212. * Binds the field with the given propertyId from the current item. If an
  213. * item has not been set then the binding is postponed until the item is set
  214. * using {@link #setItemDataSource(Item)}.
  215. * <p>
  216. * This method also adds validators when applicable.
  217. * </p>
  218. *
  219. * @param field
  220. * The field to bind
  221. * @param propertyId
  222. * The propertyId to bind to the field
  223. * @throws BindException
  224. * If the field is null or the property id is already bound to
  225. * another field by this field binder
  226. */
  227. public void bind(Field<?> field, Object propertyId) throws BindException {
  228. throwIfFieldIsNull(field, propertyId);
  229. throwIfPropertyIdAlreadyBound(field, propertyId);
  230. fieldToPropertyId.put(field, propertyId);
  231. propertyIdToField.put(propertyId, field);
  232. if (itemDataSource == null) {
  233. // Clear any possible existing binding to clear the field
  234. field.setPropertyDataSource(null);
  235. boolean fieldReadOnly = field.isReadOnly();
  236. if (!fieldReadOnly) {
  237. field.clear();
  238. } else {
  239. // Temporarily make the field read-write so we can clear the
  240. // value. Needed because setPropertyDataSource(null) does not
  241. // currently clear the field
  242. // (https://dev.vaadin.com/ticket/14733)
  243. field.setReadOnly(false);
  244. field.clear();
  245. field.setReadOnly(true);
  246. }
  247. // Will be bound when data source is set
  248. return;
  249. }
  250. field.setPropertyDataSource(
  251. wrapInTransactionalProperty(getItemProperty(propertyId)));
  252. configureField(field);
  253. }
  254. /**
  255. * Wrap property to transactional property.
  256. */
  257. protected <T> Property.Transactional<T> wrapInTransactionalProperty(
  258. Property<T> itemProperty) {
  259. return new TransactionalPropertyWrapper<T>(itemProperty);
  260. }
  261. private void throwIfFieldIsNull(Field<?> field, Object propertyId) {
  262. if (field == null) {
  263. throw new BindException(String.format(
  264. "Cannot bind property id '%s' to a null field.",
  265. propertyId));
  266. }
  267. }
  268. private void throwIfPropertyIdAlreadyBound(Field<?> field,
  269. Object propertyId) {
  270. if (propertyIdToField.containsKey(propertyId)
  271. && propertyIdToField.get(propertyId) != field) {
  272. throw new BindException("Property id " + propertyId
  273. + " is already bound to another field");
  274. }
  275. }
  276. /**
  277. * Gets the property with the given property id from the item.
  278. *
  279. * @param propertyId
  280. * The id if the property to find
  281. * @return The property with the given id from the item
  282. * @throws BindException
  283. * If the property was not found in the item or no item has been
  284. * set
  285. */
  286. protected Property getItemProperty(Object propertyId) throws BindException {
  287. Item item = getItemDataSource();
  288. if (item == null) {
  289. throw new BindException("Could not lookup property with id "
  290. + propertyId + " as no item has been set");
  291. }
  292. Property<?> p = item.getItemProperty(propertyId);
  293. if (p == null) {
  294. throw new BindException("A property with id " + propertyId
  295. + " was not found in the item");
  296. }
  297. return p;
  298. }
  299. /**
  300. * Detaches the field from its property id and removes it from this
  301. * FieldBinder.
  302. * <p>
  303. * Note that the field is not detached from its property data source if it
  304. * is no longer connected to the same property id it was bound to using this
  305. * FieldBinder.
  306. *
  307. * @param field
  308. * The field to detach
  309. * @throws BindException
  310. * If the field is not bound by this field binder or not bound
  311. * to the correct property id
  312. */
  313. public void unbind(Field<?> field) throws BindException {
  314. Object propertyId = fieldToPropertyId.get(field);
  315. if (propertyId == null) {
  316. throw new BindException(
  317. "The given field is not part of this FieldBinder");
  318. }
  319. TransactionalPropertyWrapper<?> wrapper = null;
  320. Property fieldDataSource = field.getPropertyDataSource();
  321. if (fieldDataSource instanceof TransactionalPropertyWrapper) {
  322. wrapper = (TransactionalPropertyWrapper<?>) fieldDataSource;
  323. fieldDataSource = ((TransactionalPropertyWrapper<?>) fieldDataSource)
  324. .getWrappedProperty();
  325. }
  326. if (getItemDataSource() != null
  327. && fieldDataSource == getItemProperty(propertyId)) {
  328. if (null != wrapper) {
  329. wrapper.detachFromProperty();
  330. }
  331. field.setPropertyDataSource(null);
  332. }
  333. fieldToPropertyId.remove(field);
  334. propertyIdToField.remove(propertyId);
  335. }
  336. /**
  337. * Configures a field with the settings set for this FieldBinder.
  338. * <p>
  339. * By default this updates the buffered, read only and enabled state of the
  340. * field. Also adds validators when applicable. Fields with read only data
  341. * source are always configured as read only.
  342. *
  343. * @param field
  344. * The field to update
  345. */
  346. protected void configureField(Field<?> field) {
  347. field.setBuffered(isBuffered());
  348. field.setEnabled(isEnabled());
  349. if (field.getPropertyDataSource().isReadOnly()) {
  350. field.setReadOnly(true);
  351. } else {
  352. field.setReadOnly(isReadOnly());
  353. }
  354. }
  355. /**
  356. * Gets the type of the property with the given property id.
  357. *
  358. * @param propertyId
  359. * The propertyId. Must be find
  360. * @return The type of the property
  361. */
  362. protected Class<?> getPropertyType(Object propertyId) throws BindException {
  363. if (getItemDataSource() == null) {
  364. throw new BindException("Property type for '" + propertyId
  365. + "' could not be determined. No item data source has been set.");
  366. }
  367. Property<?> p = getItemDataSource().getItemProperty(propertyId);
  368. if (p == null) {
  369. throw new BindException("Property type for '" + propertyId
  370. + "' could not be determined. No property with that id was found.");
  371. }
  372. return p.getType();
  373. }
  374. /**
  375. * Returns a collection of all property ids that have been bound to fields.
  376. * <p>
  377. * Note that this will return property ids even before the item has been
  378. * set. In that case it returns the property ids that will be bound once the
  379. * item is set.
  380. * </p>
  381. * <p>
  382. * No guarantee is given for the order of the property ids
  383. * </p>
  384. *
  385. * @return A collection of bound property ids
  386. */
  387. public Collection<Object> getBoundPropertyIds() {
  388. return Collections.unmodifiableCollection(propertyIdToField.keySet());
  389. }
  390. /**
  391. * Returns a collection of all property ids that exist in the item set using
  392. * {@link #setItemDataSource(Item)} but have not been bound to fields.
  393. * <p>
  394. * Will always return an empty collection before an item has been set using
  395. * {@link #setItemDataSource(Item)}.
  396. * </p>
  397. * <p>
  398. * No guarantee is given for the order of the property ids
  399. * </p>
  400. *
  401. * @return A collection of property ids that have not been bound to fields
  402. */
  403. public Collection<Object> getUnboundPropertyIds() {
  404. if (getItemDataSource() == null) {
  405. return new ArrayList<Object>();
  406. }
  407. List<Object> unboundPropertyIds = new ArrayList<Object>();
  408. unboundPropertyIds.addAll(getItemDataSource().getItemPropertyIds());
  409. unboundPropertyIds.removeAll(propertyIdToField.keySet());
  410. return unboundPropertyIds;
  411. }
  412. /**
  413. * Commits all changes done to the bound fields.
  414. * <p>
  415. * Calls all {@link CommitHandler}s before and after committing the field
  416. * changes to the item data source. The whole commit is aborted and state is
  417. * restored to what it was before commit was called if any
  418. * {@link CommitHandler} throws a CommitException or there is a problem
  419. * committing the fields
  420. *
  421. * @throws CommitException
  422. * If the commit was aborted
  423. */
  424. public void commit() throws CommitException {
  425. if (!isBuffered()) {
  426. // Not using buffered mode, nothing to do
  427. return;
  428. }
  429. startTransactions();
  430. try {
  431. firePreCommitEvent();
  432. Map<Field<?>, InvalidValueException> invalidValueExceptions = commitFields();
  433. if (invalidValueExceptions.isEmpty()) {
  434. firePostCommitEvent();
  435. commitTransactions();
  436. } else {
  437. throw new FieldGroupInvalidValueException(
  438. invalidValueExceptions);
  439. }
  440. } catch (Exception e) {
  441. rollbackTransactions();
  442. throw new CommitException("Commit failed", this, e);
  443. }
  444. }
  445. /**
  446. * Tries to commit all bound fields one by one and gathers any validation
  447. * exceptions in a map, which is returned to the caller
  448. *
  449. * @return a propertyId to validation exception map which is empty if all
  450. * commits succeeded
  451. */
  452. private Map<Field<?>, InvalidValueException> commitFields() {
  453. Map<Field<?>, InvalidValueException> invalidValueExceptions = new HashMap<Field<?>, InvalidValueException>();
  454. for (Field<?> f : fieldToPropertyId.keySet()) {
  455. try {
  456. f.commit();
  457. } catch (InvalidValueException e) {
  458. invalidValueExceptions.put(f, e);
  459. }
  460. }
  461. return invalidValueExceptions;
  462. }
  463. /**
  464. * Exception which wraps InvalidValueExceptions from all invalid fields in a
  465. * FieldGroup
  466. *
  467. * @since 7.4
  468. */
  469. public static class FieldGroupInvalidValueException
  470. extends InvalidValueException {
  471. private Map<Field<?>, InvalidValueException> invalidValueExceptions;
  472. /**
  473. * Constructs a new exception with the specified validation exceptions.
  474. *
  475. * @param invalidValueExceptions
  476. * a property id to exception map
  477. */
  478. public FieldGroupInvalidValueException(
  479. Map<Field<?>, InvalidValueException> invalidValueExceptions) {
  480. super(null, invalidValueExceptions.values().toArray(
  481. new InvalidValueException[invalidValueExceptions.size()]));
  482. this.invalidValueExceptions = invalidValueExceptions;
  483. }
  484. /**
  485. * Returns a map containing fields which failed validation and the
  486. * exceptions the corresponding validators threw.
  487. *
  488. * @return a map with all the invalid value exceptions
  489. */
  490. public Map<Field<?>, InvalidValueException> getInvalidFields() {
  491. return invalidValueExceptions;
  492. }
  493. }
  494. private void startTransactions() throws CommitException {
  495. for (Field<?> f : fieldToPropertyId.keySet()) {
  496. Property.Transactional<?> property = (Property.Transactional<?>) f
  497. .getPropertyDataSource();
  498. if (property == null) {
  499. throw new CommitException(
  500. "Property \"" + fieldToPropertyId.get(f)
  501. + "\" not bound to datasource.");
  502. }
  503. property.startTransaction();
  504. }
  505. }
  506. private void commitTransactions() {
  507. for (Field<?> f : fieldToPropertyId.keySet()) {
  508. ((Property.Transactional<?>) f.getPropertyDataSource()).commit();
  509. }
  510. }
  511. private void rollbackTransactions() {
  512. for (Field<?> f : fieldToPropertyId.keySet()) {
  513. try {
  514. ((Property.Transactional<?>) f.getPropertyDataSource())
  515. .rollback();
  516. } catch (Exception rollbackException) {
  517. // FIXME: What to do ?
  518. }
  519. }
  520. }
  521. /**
  522. * Sends a preCommit event to all registered commit handlers
  523. *
  524. * @throws CommitException
  525. * If the commit should be aborted
  526. */
  527. private void firePreCommitEvent() throws CommitException {
  528. CommitHandler[] handlers = commitHandlers
  529. .toArray(new CommitHandler[commitHandlers.size()]);
  530. for (CommitHandler handler : handlers) {
  531. handler.preCommit(new CommitEvent(this));
  532. }
  533. }
  534. /**
  535. * Sends a postCommit event to all registered commit handlers
  536. *
  537. * @throws CommitException
  538. * If the commit should be aborted
  539. */
  540. private void firePostCommitEvent() throws CommitException {
  541. CommitHandler[] handlers = commitHandlers
  542. .toArray(new CommitHandler[commitHandlers.size()]);
  543. for (CommitHandler handler : handlers) {
  544. handler.postCommit(new CommitEvent(this));
  545. }
  546. }
  547. /**
  548. * Discards all changes done to the bound fields.
  549. * <p>
  550. * Only has effect if buffered mode is used.
  551. *
  552. */
  553. public void discard() {
  554. for (Field<?> f : fieldToPropertyId.keySet()) {
  555. try {
  556. f.discard();
  557. } catch (Exception e) {
  558. // TODO: handle exception
  559. // What can we do if discard fails other than try to discard all
  560. // other fields?
  561. }
  562. }
  563. }
  564. /**
  565. * Returns the field that is bound to the given property id
  566. *
  567. * @param propertyId
  568. * The property id to use to lookup the field
  569. * @return The field that is bound to the property id or null if no field is
  570. * bound to that property id
  571. */
  572. public Field<?> getField(Object propertyId) {
  573. return propertyIdToField.get(propertyId);
  574. }
  575. /**
  576. * Returns the property id that is bound to the given field
  577. *
  578. * @param field
  579. * The field to use to lookup the property id
  580. * @return The property id that is bound to the field or null if the field
  581. * is not bound to any property id by this FieldBinder
  582. */
  583. public Object getPropertyId(Field<?> field) {
  584. return fieldToPropertyId.get(field);
  585. }
  586. /**
  587. * Adds a commit handler.
  588. * <p>
  589. * The commit handler is called before the field values are committed to the
  590. * item ( {@link CommitHandler#preCommit(CommitEvent)}) and after the item
  591. * has been updated ({@link CommitHandler#postCommit(CommitEvent)}). If a
  592. * {@link CommitHandler} throws a CommitException the whole commit is
  593. * aborted and the fields retain their old values.
  594. *
  595. * @param commitHandler
  596. * The commit handler to add
  597. */
  598. public void addCommitHandler(CommitHandler commitHandler) {
  599. commitHandlers.add(commitHandler);
  600. }
  601. /**
  602. * Removes the given commit handler.
  603. *
  604. * @see #addCommitHandler(CommitHandler)
  605. *
  606. * @param commitHandler
  607. * The commit handler to remove
  608. */
  609. public void removeCommitHandler(CommitHandler commitHandler) {
  610. commitHandlers.remove(commitHandler);
  611. }
  612. /**
  613. * Returns a list of all commit handlers for this {@link FieldGroup}.
  614. * <p>
  615. * Use {@link #addCommitHandler(CommitHandler)} and
  616. * {@link #removeCommitHandler(CommitHandler)} to register or unregister a
  617. * commit handler.
  618. *
  619. * @return A collection of commit handlers
  620. */
  621. protected Collection<CommitHandler> getCommitHandlers() {
  622. return Collections.unmodifiableCollection(commitHandlers);
  623. }
  624. /**
  625. * CommitHandlers are used by {@link FieldGroup#commit()} as part of the
  626. * commit transactions. CommitHandlers can perform custom operations as part
  627. * of the commit and cause the commit to be aborted by throwing a
  628. * {@link CommitException}.
  629. */
  630. public interface CommitHandler extends Serializable {
  631. /**
  632. * Called before changes are committed to the field and the item is
  633. * updated.
  634. * <p>
  635. * Throw a {@link CommitException} to abort the commit.
  636. *
  637. * @param commitEvent
  638. * An event containing information regarding the commit
  639. * @throws CommitException
  640. * if the commit should be aborted
  641. */
  642. public void preCommit(CommitEvent commitEvent) throws CommitException;
  643. /**
  644. * Called after changes are committed to the fields and the item is
  645. * updated.
  646. * <p>
  647. * Throw a {@link CommitException} to abort the commit.
  648. *
  649. * @param commitEvent
  650. * An event containing information regarding the commit
  651. * @throws CommitException
  652. * if the commit should be aborted
  653. */
  654. public void postCommit(CommitEvent commitEvent) throws CommitException;
  655. }
  656. /**
  657. * FIXME javadoc
  658. *
  659. */
  660. public static class CommitEvent implements Serializable {
  661. private FieldGroup fieldBinder;
  662. private CommitEvent(FieldGroup fieldBinder) {
  663. this.fieldBinder = fieldBinder;
  664. }
  665. /**
  666. * Returns the field binder that this commit relates to
  667. *
  668. * @return The FieldBinder that is being committed.
  669. */
  670. public FieldGroup getFieldBinder() {
  671. return fieldBinder;
  672. }
  673. }
  674. /**
  675. * Checks the validity of the bound fields.
  676. * <p>
  677. * Call the {@link Field#validate()} for the fields to get the individual
  678. * error messages.
  679. *
  680. * @return true if all bound fields are valid, false otherwise.
  681. */
  682. public boolean isValid() {
  683. try {
  684. for (Field<?> field : getFields()) {
  685. field.validate();
  686. }
  687. return true;
  688. } catch (InvalidValueException e) {
  689. return false;
  690. }
  691. }
  692. /**
  693. * Checks if any bound field has been modified.
  694. *
  695. * @return true if at least one field has been modified, false otherwise
  696. */
  697. public boolean isModified() {
  698. for (Field<?> field : getFields()) {
  699. if (field.isModified()) {
  700. return true;
  701. }
  702. }
  703. return false;
  704. }
  705. /**
  706. * Gets the field factory for the {@link FieldGroup}. The field factory is
  707. * only used when {@link FieldGroup} creates a new field.
  708. *
  709. * @return The field factory in use
  710. *
  711. */
  712. public FieldGroupFieldFactory getFieldFactory() {
  713. return fieldFactory;
  714. }
  715. /**
  716. * Sets the field factory for the {@link FieldGroup}. The field factory is
  717. * only used when {@link FieldGroup} creates a new field.
  718. *
  719. * @param fieldFactory
  720. * The field factory to use
  721. */
  722. public void setFieldFactory(FieldGroupFieldFactory fieldFactory) {
  723. this.fieldFactory = fieldFactory;
  724. }
  725. /**
  726. * Binds member fields found in the given object.
  727. * <p>
  728. * This method processes all (Java) member fields whose type extends
  729. * {@link Field} and that can be mapped to a property id. Property id
  730. * mapping is done based on the field name or on a @{@link PropertyId}
  731. * annotation on the field. All non-null fields for which a property id can
  732. * be determined are bound to the property id.
  733. * </p>
  734. * <p>
  735. * For example:
  736. *
  737. * <pre>
  738. * public class MyForm extends VerticalLayout {
  739. * private TextField firstName = new TextField("First name");
  740. * &#64;PropertyId("last")
  741. * private TextField lastName = new TextField("Last name");
  742. * private TextField age = new TextField("Age"); ... }
  743. *
  744. * MyForm myForm = new MyForm();
  745. * ...
  746. * fieldGroup.bindMemberFields(myForm);
  747. * </pre>
  748. *
  749. * </p>
  750. * This binds the firstName TextField to a "firstName" property in the item,
  751. * lastName TextField to a "last" property and the age TextField to a "age"
  752. * property.
  753. *
  754. * @param objectWithMemberFields
  755. * The object that contains (Java) member fields to bind
  756. * @throws BindException
  757. * If there is a problem binding a field
  758. */
  759. public void bindMemberFields(Object objectWithMemberFields)
  760. throws BindException {
  761. buildAndBindMemberFields(objectWithMemberFields, false);
  762. }
  763. /**
  764. * Binds member fields found in the given object and builds member fields
  765. * that have not been initialized.
  766. * <p>
  767. * This method processes all (Java) member fields whose type extends
  768. * {@link Field} and that can be mapped to a property id. Property ids are
  769. * searched in the following order: @{@link PropertyId} annotations, exact
  770. * field name matches and the case-insensitive matching that ignores
  771. * underscores. Fields that are not initialized (null) are built using the
  772. * field factory. All non-null fields for which a property id can be
  773. * determined are bound to the property id.
  774. * </p>
  775. * <p>
  776. * For example:
  777. *
  778. * <pre>
  779. * public class MyForm extends VerticalLayout {
  780. * private TextField firstName = new TextField("First name");
  781. * &#64;PropertyId("last")
  782. * private TextField lastName = new TextField("Last name");
  783. * private TextField age;
  784. *
  785. * MyForm myForm = new MyForm();
  786. * ...
  787. * fieldGroup.buildAndBindMemberFields(myForm);
  788. * </pre>
  789. *
  790. * </p>
  791. * <p>
  792. * This binds the firstName TextField to a "firstName" property in the item,
  793. * lastName TextField to a "last" property and builds an age TextField using
  794. * the field factory and then binds it to the "age" property.
  795. * </p>
  796. *
  797. * @param objectWithMemberFields
  798. * The object that contains (Java) member fields to build and
  799. * bind
  800. * @throws BindException
  801. * If there is a problem binding or building a field
  802. */
  803. public void buildAndBindMemberFields(Object objectWithMemberFields)
  804. throws BindException {
  805. buildAndBindMemberFields(objectWithMemberFields, true);
  806. }
  807. /**
  808. * Binds member fields found in the given object and optionally builds
  809. * member fields that have not been initialized.
  810. * <p>
  811. * This method processes all (Java) member fields whose type extends
  812. * {@link Field} and that can be mapped to a property id. Property ids are
  813. * searched in the following order: @{@link PropertyId} annotations, exact
  814. * field name matches and the case-insensitive matching that ignores
  815. * underscores. Fields that are not initialized (null) are built using the
  816. * field factory is buildFields is true. All non-null fields for which a
  817. * property id can be determined are bound to the property id.
  818. * </p>
  819. *
  820. * @param objectWithMemberFields
  821. * The object that contains (Java) member fields to build and
  822. * bind
  823. * @throws BindException
  824. * If there is a problem binding or building a field
  825. */
  826. protected void buildAndBindMemberFields(Object objectWithMemberFields,
  827. boolean buildFields) throws BindException {
  828. Class<?> objectClass = objectWithMemberFields.getClass();
  829. for (java.lang.reflect.Field memberField : getFieldsInDeclareOrder(
  830. objectClass)) {
  831. if (!Field.class.isAssignableFrom(memberField.getType())) {
  832. // Process next field
  833. continue;
  834. }
  835. PropertyId propertyIdAnnotation = memberField
  836. .getAnnotation(PropertyId.class);
  837. Class<? extends Field> fieldType = (Class<? extends Field>) memberField
  838. .getType();
  839. Object propertyId = null;
  840. if (propertyIdAnnotation != null) {
  841. // @PropertyId(propertyId) always overrides property id
  842. propertyId = propertyIdAnnotation.value();
  843. } else {
  844. try {
  845. propertyId = findPropertyId(memberField);
  846. } catch (SearchException e) {
  847. // Property id was not found, skip this field
  848. continue;
  849. }
  850. if (propertyId == null) {
  851. // Property id was not found, skip this field
  852. continue;
  853. }
  854. }
  855. // Ensure that the property id exists
  856. Class<?> propertyType;
  857. try {
  858. propertyType = getPropertyType(propertyId);
  859. } catch (BindException e) {
  860. // Property id was not found, skip this field
  861. continue;
  862. }
  863. Field<?> field;
  864. try {
  865. // Get the field from the object
  866. field = (Field<?>) ReflectTools.getJavaFieldValue(
  867. objectWithMemberFields, memberField, Field.class);
  868. } catch (Exception e) {
  869. // If we cannot determine the value, just skip the field and try
  870. // the next one
  871. continue;
  872. }
  873. if (field == null && buildFields) {
  874. Caption captionAnnotation = memberField
  875. .getAnnotation(Caption.class);
  876. String caption;
  877. if (captionAnnotation != null) {
  878. caption = captionAnnotation.value();
  879. } else {
  880. caption = DefaultFieldFactory
  881. .createCaptionByPropertyId(propertyId);
  882. }
  883. // Create the component (LegacyField)
  884. field = build(caption, propertyType, fieldType);
  885. // Store it in the field
  886. try {
  887. ReflectTools.setJavaFieldValue(objectWithMemberFields,
  888. memberField, field);
  889. } catch (IllegalArgumentException e) {
  890. throw new BindException("Could not assign value to field '"
  891. + memberField.getName() + "'", e);
  892. } catch (IllegalAccessException e) {
  893. throw new BindException("Could not assign value to field '"
  894. + memberField.getName() + "'", e);
  895. } catch (InvocationTargetException e) {
  896. throw new BindException("Could not assign value to field '"
  897. + memberField.getName() + "'", e);
  898. }
  899. }
  900. if (field != null) {
  901. // Bind it to the property id
  902. bind(field, propertyId);
  903. }
  904. }
  905. }
  906. /**
  907. * Searches for a property id from the current itemDataSource that matches
  908. * the given memberField.
  909. * <p>
  910. * If perfect match is not found, uses a case insensitive search that also
  911. * ignores underscores. Returns null if no match is found. Throws a
  912. * SearchException if no item data source has been set.
  913. * </p>
  914. * <p>
  915. * The propertyId search logic used by
  916. * {@link #buildAndBindMemberFields(Object, boolean)
  917. * buildAndBindMemberFields} can easily be customized by overriding this
  918. * method. No other changes are needed.
  919. * </p>
  920. *
  921. * @param memberField
  922. * The field an object id is searched for
  923. * @return
  924. */
  925. protected Object findPropertyId(java.lang.reflect.Field memberField) {
  926. String fieldName = memberField.getName();
  927. if (getItemDataSource() == null) {
  928. throw new SearchException("Property id type for field '" + fieldName
  929. + "' could not be determined. No item data source has been set.");
  930. }
  931. Item dataSource = getItemDataSource();
  932. if (dataSource.getItemProperty(fieldName) != null) {
  933. return fieldName;
  934. } else {
  935. String minifiedFieldName = minifyFieldName(fieldName);
  936. for (Object itemPropertyId : dataSource.getItemPropertyIds()) {
  937. if (itemPropertyId instanceof String) {
  938. String itemPropertyName = (String) itemPropertyId;
  939. if (minifiedFieldName
  940. .equals(minifyFieldName(itemPropertyName))) {
  941. return itemPropertyName;
  942. }
  943. }
  944. }
  945. }
  946. return null;
  947. }
  948. protected static String minifyFieldName(String fieldName) {
  949. return fieldName.toLowerCase().replace("_", "");
  950. }
  951. /**
  952. * Exception thrown by a FieldGroup when the commit operation fails.
  953. *
  954. * Provides information about validation errors through
  955. * {@link #getInvalidFields()} if the cause of the failure is that all bound
  956. * fields did not pass validation
  957. *
  958. */
  959. public static class CommitException extends Exception {
  960. private FieldGroup fieldGroup;
  961. public CommitException() {
  962. super();
  963. }
  964. public CommitException(String message, FieldGroup fieldGroup,
  965. Throwable cause) {
  966. super(message, cause);
  967. this.fieldGroup = fieldGroup;
  968. }
  969. public CommitException(String message, Throwable cause) {
  970. super(message, cause);
  971. }
  972. public CommitException(String message) {
  973. super(message);
  974. }
  975. public CommitException(Throwable cause) {
  976. super(cause);
  977. }
  978. /**
  979. * Returns a map containing the fields which failed validation and the
  980. * exceptions the corresponding validators threw.
  981. *
  982. * @since 7.4
  983. * @return a map with all the invalid value exceptions. Can be empty but
  984. * not null
  985. */
  986. public Map<Field<?>, InvalidValueException> getInvalidFields() {
  987. if (getCause() instanceof FieldGroupInvalidValueException) {
  988. return ((FieldGroupInvalidValueException) getCause())
  989. .getInvalidFields();
  990. }
  991. return new HashMap<Field<?>, InvalidValueException>();
  992. }
  993. /**
  994. * Returns the field group where the exception occurred
  995. *
  996. * @since 7.4
  997. * @return the field group
  998. */
  999. public FieldGroup getFieldGroup() {
  1000. return fieldGroup;
  1001. }
  1002. }
  1003. public static class BindException extends RuntimeException {
  1004. public BindException(String message) {
  1005. super(message);
  1006. }
  1007. public BindException(String message, Throwable t) {
  1008. super(message, t);
  1009. }
  1010. }
  1011. public static class SearchException extends RuntimeException {
  1012. public SearchException(String message) {
  1013. super(message);
  1014. }
  1015. public SearchException(String message, Throwable t) {
  1016. super(message, t);
  1017. }
  1018. }
  1019. /**
  1020. * Builds a field and binds it to the given property id using the field
  1021. * binder.
  1022. *
  1023. * @param propertyId
  1024. * The property id to bind to. Must be present in the field
  1025. * finder.
  1026. * @throws BindException
  1027. * If there is a problem while building or binding
  1028. * @return The created and bound field
  1029. */
  1030. public Field<?> buildAndBind(Object propertyId) throws BindException {
  1031. String caption = DefaultFieldFactory
  1032. .createCaptionByPropertyId(propertyId);
  1033. return buildAndBind(caption, propertyId);
  1034. }
  1035. /**
  1036. * Builds a field using the given caption and binds it to the given property
  1037. * id using the field binder.
  1038. *
  1039. * @param caption
  1040. * The caption for the field
  1041. * @param propertyId
  1042. * The property id to bind to. Must be present in the field
  1043. * finder.
  1044. * @throws BindException
  1045. * If there is a problem while building or binding
  1046. * @return The created and bound field. Can be any type of {@link Field}.
  1047. */
  1048. public Field<?> buildAndBind(String caption, Object propertyId)
  1049. throws BindException {
  1050. return buildAndBind(caption, propertyId, Field.class);
  1051. }
  1052. /**
  1053. * Builds a field using the given caption and binds it to the given property
  1054. * id using the field binder. Ensures the new field is of the given type.
  1055. *
  1056. * @param caption
  1057. * The caption for the field
  1058. * @param propertyId
  1059. * The property id to bind to. Must be present in the field
  1060. * finder.
  1061. * @throws BindException
  1062. * If the field could not be created
  1063. * @return The created and bound field. Can be any type of {@link Field}.
  1064. */
  1065. public <T extends Field> T buildAndBind(String caption, Object propertyId,
  1066. Class<T> fieldType) throws BindException {
  1067. Class<?> type = getPropertyType(propertyId);
  1068. T field = build(caption, type, fieldType);
  1069. bind(field, propertyId);
  1070. return field;
  1071. }
  1072. /**
  1073. * Creates a field based on the given data type.
  1074. * <p>
  1075. * The data type is the type that we want to edit using the field. The field
  1076. * type is the type of field we want to create, can be {@link Field} if any
  1077. * LegacyField is good.
  1078. * </p>
  1079. *
  1080. * @param caption
  1081. * The caption for the new field
  1082. * @param dataType
  1083. * The data model type that we want to edit using the field
  1084. * @param fieldType
  1085. * The type of field that we want to create
  1086. * @return A LegacyField capable of editing the given type
  1087. * @throws BindException
  1088. * If the field could not be created
  1089. */
  1090. protected <T extends Field> T build(String caption, Class<?> dataType,
  1091. Class<T> fieldType) throws BindException {
  1092. T field = getFieldFactory().createField(dataType, fieldType);
  1093. if (field == null) {
  1094. throw new BindException(
  1095. "Unable to build a field of type " + fieldType.getName()
  1096. + " for editing " + dataType.getName());
  1097. }
  1098. field.setCaption(caption);
  1099. return field;
  1100. }
  1101. /**
  1102. * Returns an array containing LegacyField objects reflecting all the fields
  1103. * of the class or interface represented by this Class object. The elements
  1104. * in the array returned are sorted in declare order from sub class to super
  1105. * class.
  1106. *
  1107. * @param searchClass
  1108. * @return
  1109. */
  1110. protected static List<java.lang.reflect.Field> getFieldsInDeclareOrder(
  1111. Class searchClass) {
  1112. ArrayList<java.lang.reflect.Field> memberFieldInOrder = new ArrayList<java.lang.reflect.Field>();
  1113. while (searchClass != null) {
  1114. for (java.lang.reflect.Field memberField : searchClass
  1115. .getDeclaredFields()) {
  1116. memberFieldInOrder.add(memberField);
  1117. }
  1118. searchClass = searchClass.getSuperclass();
  1119. }
  1120. return memberFieldInOrder;
  1121. }
  1122. /**
  1123. * Clears the value of all fields.
  1124. *
  1125. * @since 7.4
  1126. */
  1127. public void clear() {
  1128. for (Field<?> f : getFields()) {
  1129. if (f instanceof AbstractField) {
  1130. ((AbstractField) f).clear();
  1131. }
  1132. }
  1133. }
  1134. }