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    }