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 }