001 package org.springframework.richclient.widget.editor; 002 003 import org.apache.commons.logging.Log; 004 import org.apache.commons.logging.LogFactory; 005 import org.jdesktop.swingworker.SwingWorker; 006 import org.springframework.binding.validation.support.DefaultValidationMessage; 007 import org.springframework.binding.validation.support.DefaultValidationResultsModel; 008 import org.springframework.richclient.application.Application; 009 import org.springframework.richclient.application.session.ApplicationSession; 010 import org.springframework.richclient.application.statusbar.StatusBar; 011 import org.springframework.richclient.application.statusbar.support.StatusBarProgressMonitor; 012 import org.springframework.richclient.command.AbstractCommand; 013 import org.springframework.richclient.core.DefaultMessage; 014 import org.springframework.richclient.core.Severity; 015 import org.springframework.richclient.form.AbstractForm; 016 import org.springframework.richclient.form.FilterForm; 017 import org.springframework.richclient.util.RcpSupport; 018 import org.springframework.richclient.widget.AbstractWidget; 019 import org.springframework.richclient.widget.Widget; 020 import org.springframework.richclient.widget.editor.provider.DataProvider; 021 import org.springframework.richclient.widget.editor.provider.DataProviderEvent; 022 import org.springframework.richclient.widget.editor.provider.DataProviderListener; 023 import org.springframework.richclient.widget.editor.provider.MaximumRowsExceededException; 024 import org.springframework.richclient.widget.table.TableDescription; 025 import org.springframework.richclient.widget.table.TableWidget; 026 import org.springframework.richclient.widget.table.glazedlists.GlazedListTableWidget; 027 028 import javax.swing.*; 029 import java.beans.PropertyChangeEvent; 030 import java.beans.PropertyChangeListener; 031 import java.util.*; 032 import java.util.concurrent.ExecutionException; 033 034 /** 035 * DefaultDataEditorWidget is a basic implementation of a 036 * {@link org.springframework.richclient.widget.editor.AbstractDataEditorWidget}. 037 */ 038 public class DefaultDataEditorWidget extends AbstractDataEditorWidget 039 implements 040 DataProviderListener, 041 PropertyChangeListener 042 { 043 044 private static Log log = LogFactory.getLog(DefaultDataEditorWidget.class); 045 046 /** 047 * Detailform of this dataeditor (under table). 048 */ 049 private AbstractForm detailForm; 050 051 /** 052 * Filterform of this dataeditor (next to table). 053 */ 054 private FilterForm filterForm; 055 056 /** 057 * Table with data objects 058 */ 059 private TableWidget tableWidget; 060 061 /** 062 * Constant to be used to embed a dataEditor parameterMap in a command parameterMap. 063 */ 064 public static final String PARAMETER_MAP = "dataEditorParameters"; 065 066 /** 067 * Parameter to provide a filter. 068 */ 069 public static final String PARAMETER_FILTER = "filter"; 070 071 /** 072 * Parameter to provide a default selected object. 073 */ 074 public static final String PARAMETER_DEFAULT_SELECTED_OBJECT = "defaultSelectedObject"; 075 076 /** 077 * DataProvider manages data access and determines CRUD capabilities. 078 */ 079 private DataProvider dataProvider; 080 081 /** 082 * {@link org.springframework.binding.validation.ValidationResultsModel} combining results from embedded forms and other messages. 083 */ 084 private final DefaultValidationResultsModel validationResultsModel = new DefaultValidationResultsModel(); 085 086 private ListRetrievingWorker listWorker; 087 088 private final MaximumRowsExceededMessage maximumRowsExceededMessage = new MaximumRowsExceededMessage(); 089 090 protected static class MaximumRowsExceededMessage extends DefaultValidationMessage 091 { 092 093 private String message; 094 095 public MaximumRowsExceededMessage() 096 { 097 super("maximumRowsExceeded", Severity.WARNING, "maximumRowsExceeded"); 098 } 099 100 @Override 101 public String getMessage() 102 { 103 return message; 104 } 105 106 public void setMessage(String message) 107 { 108 this.message = message; 109 } 110 111 } 112 113 /** 114 * {@link SwingWorker} which retrieves list from back-end and fills table with result. 115 * <p/> 116 * Remember to set criteria and launch this class in a synchronised block. 117 */ 118 private class ListRetrievingWorker extends SwingWorker<List<Object>, String> 119 { 120 121 /** 122 * The filter criteria to use. 123 */ 124 protected Object filterCriteria; 125 126 /** 127 * Additional parameters that are coupled with this instance/run. 128 */ 129 protected Map<String, Object> parameters; 130 131 @Override 132 protected List<Object> doInBackground() throws Exception 133 { 134 return getDataProvider().getList(filterCriteria); 135 } 136 137 /** 138 * Set the rows in the table. 139 */ 140 @Override 141 protected void done() 142 { 143 try 144 { 145 listWorkerDone(get(), parameters); 146 } 147 catch (InterruptedException e) 148 { 149 // someone cancelled the retrieval? 150 } 151 catch (ExecutionException e) 152 { 153 if (e.getCause() instanceof MaximumRowsExceededException) 154 { 155 MaximumRowsExceededException mre = (MaximumRowsExceededException) e.getCause(); 156 setRows(Collections.EMPTY_LIST); 157 validationResultsModel.removeMessage(maximumRowsExceededMessage); 158 maximumRowsExceededMessage.setMessage(getMessage("MaximumRowsExceededException.notice", new Object[] {mre.getNumberOfRows(), mre.getMaxRows()})); 159 validationResultsModel.addMessage(maximumRowsExceededMessage); 160 if (getToggleFilterCommand() != null) 161 { 162 getToggleFilterCommand().doShow(); 163 } 164 } 165 else 166 { 167 throw new RuntimeException(e); 168 } 169 } 170 finally 171 { 172 Application.instance().getActiveWindow().getStatusBar().getProgressMonitor().done(); 173 // getFilterForm().getCommitCommand().setEnabled(true); 174 // getRefreshCommand().setEnabled(true); 175 listWorker = null; 176 } 177 } 178 } 179 180 /** 181 * This method is called on the gui-thread when the worker ends. As default it will check for the 182 * PARAMETER_DEFAULT_SELECTED_OBJECT parameter in the map. 183 * 184 * @param rows fetched by the listWorker. 185 * @param parameters a map of parameters specific to this listWorker instance. 186 */ 187 protected void listWorkerDone(List<Object> rows, Map<String, Object> parameters) 188 { 189 setRows(rows); 190 // remove maximumRowsExceededMessages if needed 191 validationResultsModel.removeMessage(maximumRowsExceededMessage); 192 if ((rows == null) || (rows.size() == 0)) 193 { 194 return; 195 } 196 197 Object defaultSelectedObject = null; 198 if (parameters.containsKey(PARAMETER_DEFAULT_SELECTED_OBJECT)) 199 { 200 defaultSelectedObject = parameters.get(PARAMETER_DEFAULT_SELECTED_OBJECT); 201 } 202 203 if (defaultSelectedObject == null) 204 { 205 tableWidget.selectRowObject(0, null); 206 } 207 else 208 { 209 tableWidget.selectRowObject(defaultSelectedObject, null); 210 } 211 } 212 213 /** 214 * Default constructor. Add id, {@link DataProvider}, {@link org.springframework.richclient.form.Form}s and listView later. 215 * 216 * @see #setDataProvider(DataProvider) 217 * @see #setDetailForm(AbstractForm) 218 * @see #setFilterForm(FilterForm) 219 * @see #setId(String) 220 * @see #setTableWidget(TableDescription) 221 * @see #setTableWidget(TableWidget) 222 */ 223 public DefaultDataEditorWidget() 224 { 225 } 226 227 /** 228 * Constructor with id and {@link DataProvider}. Add {@link org.springframework.richclient.form.Form}s and listView later. 229 * 230 * @param id used to fetch messages/icons. 231 * @param provider provides the data manipulation and possible CRUD options. 232 * @see #setDetailForm(AbstractForm) 233 * @see #setFilterForm(FilterForm) 234 * @see #setTableWidget(TableDescription) 235 * @see #setTableWidget(TableWidget) 236 */ 237 public DefaultDataEditorWidget(String id, DataProvider provider) 238 { 239 this(id, provider, null, null, null); 240 } 241 242 public DefaultDataEditorWidget(DataProvider provider, AbstractForm form, TableDescription tableDesc, 243 FilterForm filterForm) 244 { 245 this(null, provider, form, tableDesc, filterForm); 246 } 247 248 /** 249 * Constructor allowing to set all major components at once. 250 * 251 * @param id used to fetch messages/icons. 252 * @param provider provides the data manipulation and possible CRUD options. 253 * @param form used to display and edit one row detail. 254 * @param tableDesc describes the columns of the table to build. 255 * @param filterForm optional form used to filter the data. 256 */ 257 public DefaultDataEditorWidget(String id, DataProvider provider, AbstractForm form, 258 TableDescription tableDesc, FilterForm filterForm) 259 { 260 setId(id); 261 setDataProvider(provider); 262 setDetailForm(form); 263 setTableWidget(tableDesc); 264 setFilterForm(filterForm); 265 } 266 267 @Override 268 public void setTitle(String title) 269 { 270 super.setTitle(title); 271 tableWidget.getTable().setName(title); 272 } 273 274 /** 275 * Returns only the detail form widget 276 */ 277 @Override 278 public Widget createDetailWidget() 279 { 280 return new AbstractWidget() 281 { 282 283 @Override 284 public void onAboutToShow() 285 { 286 DefaultDataEditorWidget.this.onAboutToShow(); 287 } 288 289 @Override 290 public void onAboutToHide() 291 { 292 DefaultDataEditorWidget.this.onAboutToHide(); 293 } 294 295 public JComponent getComponent() 296 { 297 return getDetailForm().getControl(); 298 } 299 300 @Override 301 public List<? extends AbstractCommand> getCommands() 302 { 303 return Arrays.asList(getDetailForm().getCommitCommand()); 304 } 305 }; 306 } 307 308 /** 309 * Set the form that will handle one detail item. 310 */ 311 protected void setDetailForm(AbstractForm detailForm) 312 { 313 if (this.detailForm != null) 314 { 315 validationResultsModel.remove(this.detailForm.getFormModel().getValidationResults()); 316 } 317 318 this.detailForm = detailForm; 319 320 if (this.detailForm != null) 321 { 322 validationResultsModel.add(this.detailForm.getFormModel().getValidationResults()); 323 } 324 } 325 326 @Override 327 public AbstractForm getDetailForm() 328 { 329 return this.detailForm; 330 } 331 332 /** 333 * Set the form to use as filter. 334 * 335 * @see DataProvider#supportsFiltering() 336 */ 337 protected void setFilterForm(FilterForm filterForm) 338 { 339 if (this.filterForm != null) 340 { 341 validationResultsModel.remove(this.filterForm.getFormModel().getValidationResults()); 342 } 343 344 this.filterForm = filterForm; 345 346 if (this.filterForm != null) 347 { 348 validationResultsModel.add(filterForm.getFormModel().getValidationResults()); 349 } 350 } 351 352 @Override 353 public FilterForm getFilterForm() 354 { 355 return this.filterForm; 356 } 357 358 public void setFilterModel(Object model) 359 { 360 getFilterForm().setFormObject(model); 361 } 362 363 /** 364 * Create a {@link GlazedListTableWidget} based on the given {@link TableDescription} to be used as 365 * listView. 366 * 367 * @param tableDescription description of columns used to create the table. 368 */ 369 protected void setTableWidget(TableDescription tableDescription) 370 { 371 if (tableDescription != null) 372 { 373 TableWidget tableWidget = new GlazedListTableWidget(null, tableDescription); 374 setTableWidget(tableWidget); 375 } 376 } 377 378 /** 379 * Set the listView of this dataEditor. 380 */ 381 protected void setTableWidget(TableWidget tableWidget) 382 { 383 if (this.tableWidget != null) 384 { 385 this.tableWidget.removeSelectionObserver(tableSelectionObserver); 386 } 387 388 this.tableWidget = tableWidget; 389 390 if (this.tableWidget != null) 391 { 392 this.tableWidget.addSelectionObserver(tableSelectionObserver); 393 } 394 } 395 396 @Override 397 public TableWidget getTableWidget() 398 { 399 return this.tableWidget; 400 } 401 402 /** 403 * Set the provider to use for data manipulation. 404 */ 405 protected void setDataProvider(DataProvider provider) 406 { 407 if ((this.dataProvider != null) 408 && (this.dataProvider.getRefreshPolicy() == DataProvider.RefreshPolicy.ON_USER_SWITCH)) 409 { 410 ApplicationSession.getSession().removePropertyChangeListener(ApplicationSession.USER, this); 411 } 412 413 this.dataProvider = provider; 414 415 if ((this.dataProvider != null) 416 && (this.dataProvider.getRefreshPolicy() == DataProvider.RefreshPolicy.ON_USER_SWITCH)) 417 { 418 ApplicationSession.getSession().addPropertyChangeListener(ApplicationSession.USER, this); 419 } 420 } 421 422 public DataProvider getDataProvider() 423 { 424 return dataProvider; 425 } 426 427 @Override 428 protected boolean isUpdateRowSupported() 429 { 430 return this.dataProvider.supportsUpdate(); 431 } 432 433 @Override 434 protected boolean isAddRowSupported() 435 { 436 return this.dataProvider.supportsCreate(); 437 } 438 439 @Override 440 protected boolean isCloneRowSupported() 441 { 442 return this.dataProvider.supportsClone() && this.dataProvider.supportsCreate(); 443 } 444 445 @Override 446 protected boolean isFilterSupported() 447 { 448 return this.dataProvider.supportsFiltering(); 449 } 450 451 @Override 452 protected boolean isRemoveRowsSupported() 453 { 454 return this.dataProvider.supportsDelete(); 455 } 456 457 /** 458 * Executes filter and fills table in specific manner: 459 * <p/> 460 * <ul> 461 * <li>set baseCriteria if needed</li> 462 * <li>set searchCriteria on filterForm</li> 463 * <li>set searchCriteria on worker</li> 464 * <li>pass parameter map to worker</li> 465 * <li>launch worker to retrieve list from back-end and fill table</li> 466 * <li>when done, set list and execute additional code taking the parameters into account</li> 467 * </ul> 468 * 469 * @param parameters a number of parameters that can influence this run. Should be a non-modifiable map or a 470 * specific instance. 471 */ 472 @Override 473 public synchronized void executeFilter(Map<String, Object> parameters) 474 { 475 if (listWorker == null) 476 { 477 if (dataProvider.supportsBaseCriteria()) 478 { 479 dataProvider.setBaseCriteria(getBaseCriteria()); 480 } 481 482 StatusBar statusBar = Application.instance().getActiveWindow().getStatusBar(); 483 statusBar.getProgressMonitor().taskStarted( 484 RcpSupport.getMessage("statusBar", "loadTable", RcpSupport.LABEL), 485 StatusBarProgressMonitor.UNKNOWN); 486 // getFilterForm().getCommitCommand().setEnabled(false); 487 // getRefreshCommand().setEnabled(false); 488 489 listWorker = new ListRetrievingWorker(); 490 if (dataProvider.supportsFiltering()) 491 { 492 if (parameters.containsKey(PARAMETER_FILTER)) 493 { 494 setFilterModel(parameters.get(PARAMETER_FILTER)); 495 } 496 497 listWorker.filterCriteria = getFilterForm().getFilterCriteria(); 498 } 499 500 listWorker.parameters = parameters; 501 log.debug("Execute Filter with criteria: " + listWorker.filterCriteria + " and parameters: " 502 + parameters); 503 listWorker.execute(); 504 } 505 } 506 507 /** 508 * @see #executeFilter(Map) 509 */ 510 @Override 511 public void executeFilter() 512 { 513 executeFilter(Collections.EMPTY_MAP); 514 } 515 516 /** 517 * <b>Warning!</b> this can block threads for an extended period, make sure you're aware of this. 518 * <p/> 519 * <p> 520 * Alternative: {@link #executeFilter()} will launch separate worker and fills table. 521 * </p> 522 * 523 * @param criteria 524 * @return 525 */ 526 protected List getList(Object criteria) 527 { 528 if (this.dataProvider.supportsBaseCriteria()) 529 { 530 this.dataProvider.setBaseCriteria(getBaseCriteria()); 531 } 532 try 533 { 534 List dataSet = this.dataProvider.getList(criteria); 535 setRows(dataSet); 536 setMessage(null); 537 return dataSet; 538 } 539 catch (MaximumRowsExceededException mre) 540 { 541 setRows(Collections.EMPTY_LIST); 542 setMessage(new DefaultMessage(getMessage("MaximumRowsExceededException.notice", new Object[] {mre.getNumberOfRows(), mre.getMaxRows()}), Severity.WARNING)); 543 if (getToggleFilterCommand() != null) 544 { 545 getToggleFilterCommand().doShow(); 546 } 547 return null; 548 } 549 } 550 551 /** 552 * Internal fill method of the datatable 553 * <p/> 554 * WARNING: not threadsafe, please call me on the EDT! 555 */ 556 protected void setRows(List dataSet) 557 { 558 tableWidget.setRows(dataSet); 559 } 560 561 protected Object getBaseCriteria() 562 { 563 return null; 564 } 565 566 @Override 567 protected Object loadEntityDetails(Object baseObject, boolean forceLoad) 568 { 569 return this.dataProvider.getDetailObject(baseObject, forceLoad); 570 } 571 572 public Object loadSimpleEntity(Object baseObject) 573 { 574 Object returnValue = this.dataProvider.getSimpleObject(baseObject); 575 if (returnValue == null) 576 { 577 throw new NullPointerException("Returnvalue for dataprovider simple was null"); 578 } 579 return returnValue; 580 } 581 582 @Override 583 protected Object saveEntity(Object dirtyObject) 584 { 585 if (!this.dataProvider.supportsUpdate()) 586 { 587 return null; 588 } 589 return this.dataProvider.update(dirtyObject); 590 } 591 592 @Override 593 protected void newRow(Object newClone) 594 { 595 if (newClone == null) 596 { 597 super.newRow(getDataProvider().newInstance( 598 getFilterForm() == null ? null : getFilterForm().getFormObject())); 599 } 600 else 601 { 602 super.newRow(newClone); 603 } 604 } 605 606 @Override 607 protected Object createNewEntity(Object newObject) 608 { 609 if (!this.dataProvider.supportsCreate()) 610 { 611 return null; 612 } 613 return this.dataProvider.create(newObject); 614 } 615 616 @Override 617 protected Object cloneEntity(Object sampleObject) 618 { 619 if (!this.dataProvider.supportsClone()) 620 { 621 return null; 622 } 623 return this.dataProvider.clone(sampleObject); 624 } 625 626 @Override 627 protected void removeEntity(Object objectToRemove) 628 { 629 if (!this.dataProvider.supportsDelete()) 630 { 631 return; 632 } 633 this.dataProvider.delete(objectToRemove); 634 } 635 636 public void update(Observable o, Object arg) 637 { 638 if (arg instanceof DataProviderEvent) 639 { 640 DataProviderEvent obsAct = (DataProviderEvent) arg; 641 int act = obsAct.getEventType(); 642 if (act == DataProviderEvent.EVENT_TYPE_NEW) 643 { 644 this.tableWidget.addRowObject(obsAct.getNewEntity()); 645 } 646 else if (act == DataProviderEvent.EVENT_TYPE_UPDATE) 647 { 648 replaceRowObject(obsAct.getOldEntity(), obsAct.getNewEntity()); 649 } 650 else if (act == DataProviderEvent.EVENT_TYPE_DELETE) 651 { 652 this.tableWidget.removeRowObject(obsAct.getOldEntity()); 653 } 654 } 655 } 656 657 /** 658 * {@inheritDoc} 659 */ 660 @Override 661 public void onAboutToShow() 662 { 663 log.debug(getId() + ": onAboutToShow with refreshPolicy: " + dataProvider.getRefreshPolicy()); 664 super.onAboutToShow(); 665 666 dataProvider.addDataProviderListener(this); 667 registerListeners(); 668 if (detailForm instanceof Widget) 669 { 670 ((Widget) detailForm).onAboutToShow(); 671 } 672 673 tableWidget.onAboutToShow(); 674 // lazy loading, if no list is present, load when widget is shown 675 // include RefreshPolicy given by DataProvider 676 if ((dataProvider.getRefreshPolicy() != DataProvider.RefreshPolicy.NEVER) && (tableWidget.isEmpty())) 677 { 678 executeFilter(); 679 } 680 else if (!tableWidget.hasSelection()) 681 { 682 tableWidget.selectRowObject(0, this); 683 } 684 } 685 686 /** 687 * {@inheritDoc} 688 */ 689 @Override 690 public void onAboutToHide() 691 { 692 log.debug(getId() + ": onAboutToHide with refreshPolicy: " + dataProvider.getRefreshPolicy()); 693 super.onAboutToHide(); 694 695 this.dataProvider.removeDataProviderListener(this); 696 unRegisterListeners(); 697 if (detailForm instanceof Widget) 698 { 699 ((Widget) detailForm).onAboutToHide(); 700 } 701 702 if (dataProvider.getRefreshPolicy() == DataProvider.RefreshPolicy.ALLWAYS) 703 { 704 getTableWidget().setRows(Collections.EMPTY_LIST); 705 } 706 } 707 708 protected void registerListeners() 709 { 710 } 711 712 protected void unRegisterListeners() 713 { 714 } 715 716 /** 717 * note: differs from previous method to allow setting of formObject on filterForm. Will probably end up 718 * in refactoring of dataeditorwidget. 719 * 720 * @param criteria formObject to set on FilterForm. 721 * @return 722 */ 723 public Object setSelectedSearch(Object criteria) 724 { 725 // filterField leegmaken 726 if (tableWidget.getTextFilterField() != null) 727 { 728 tableWidget.getTextFilterField().setText(""); 729 } 730 // if Referable == null, empty filterForm and execute filter 731 if (criteria == null) 732 { 733 if (dataProvider.supportsFiltering()) 734 { 735 getFilterForm().getNewFormObjectCommand().execute(); 736 } 737 executeFilter(); 738 return null; 739 } 740 List resultList = getList(criteria); 741 if (dataProvider.supportsFiltering()) 742 { 743 // adapt filterForm to reflect referable criteria 744 if ((resultList == null) || (resultList.size() > 0)) // fill in referable 745 { 746 getFilterForm().setFormObject(criteria); 747 } 748 else 749 { // empty filterForm and execute 750 getFilterForm().getNewFormObjectCommand().execute(); 751 executeFilter(); 752 } 753 } 754 if (resultList != null && resultList.size() == 1) 755 { 756 // return the detailObject 757 // return loadEntityDetails(resultList.get(0)); 758 return loadSimpleEntity(resultList.get(0)); 759 } 760 return resultList; 761 } 762 763 public void refreshSelectedObject() 764 { 765 Object selected = getSelectedRowObject(); 766 getTableWidget().selectRowObject(-1, null); 767 getTableWidget().selectRowObject(selected, null); 768 } 769 770 public void propertyChange(PropertyChangeEvent evt) 771 { 772 if ((dataProvider.getRefreshPolicy() == DataProvider.RefreshPolicy.ALLWAYS) 773 || (dataProvider.getRefreshPolicy() == DataProvider.RefreshPolicy.ON_USER_SWITCH)) 774 { 775 log.debug("USER changed event, refreshPolicy= " + dataProvider.getRefreshPolicy()); 776 777 if ((evt.getNewValue() == null)) 778 { 779 setRows(Collections.EMPTY_LIST); 780 } 781 else if (isShowing()) 782 { 783 executeFilter(); 784 } 785 } 786 } 787 788 @Override 789 protected final DefaultValidationResultsModel getValidationResults() 790 { 791 return validationResultsModel; 792 } 793 }