/**
* Copyright (C) 2015 Valkyrie RCP
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.valkyriercp.widget.editor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert;
import org.valkyriercp.application.StatusBar;
import org.valkyriercp.application.session.ApplicationSession;
import org.valkyriercp.application.support.StatusBarProgressMonitor;
import org.valkyriercp.binding.validation.support.DefaultValidationMessage;
import org.valkyriercp.binding.validation.support.DefaultValidationResultsModel;
import org.valkyriercp.command.support.AbstractCommand;
import org.valkyriercp.core.DefaultMessage;
import org.valkyriercp.core.Severity;
import org.valkyriercp.form.AbstractForm;
import org.valkyriercp.form.FilterForm;
import org.valkyriercp.util.CachedCallable;
import org.valkyriercp.util.MessageConstants;
import org.valkyriercp.widget.AbstractWidget;
import org.valkyriercp.widget.Widget;
import org.valkyriercp.widget.editor.provider.DataProvider;
import org.valkyriercp.widget.editor.provider.DataProviderEvent;
import org.valkyriercp.widget.editor.provider.DataProviderListener;
import org.valkyriercp.widget.editor.provider.MaximumRowsExceededException;
import org.valkyriercp.widget.table.TableDescription;
import org.valkyriercp.widget.table.TableWidget;
import org.valkyriercp.widget.table.glazedlists.GlazedListTableWidget;
import javax.swing.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.*;
import java.util.concurrent.ExecutionException;
//import org.jdesktop.swingworker.SwingWorker;
/**
* DefaultDataEditorWidget is a basic implementation of a
* {@link AbstractDataEditorWidget}.
*/
public class DefaultDataEditorWidget extends AbstractDataEditorWidget implements
DataProviderListener, PropertyChangeListener {
private static Log log = LogFactory.getLog(DefaultDataEditorWidget.class);
/**
* Detailform of this dataeditor (under table).
*/
private AbstractForm detailForm;
/**
* Filterform of this dataeditor (next to table).
*/
private FilterForm filterForm;
/**
* Table with data objects
*/
private CachedCallable<TableWidget> tableWidget;
/**
* Constant to be used to embed a dataEditor parameterMap in a command
* parameterMap.
*/
public static final String PARAMETER_MAP = "dataEditorParameters";
/**
* Parameter to provide a filter.
*/
public static final String PARAMETER_FILTER = "filter";
/**
* Parameter to provide a default selected object.
*/
public static final String PARAMETER_DEFAULT_SELECTED_OBJECT = "defaultSelectedObject";
/**
* DataProvider manages data access and determines CRUD capabilities.
*/
private DataProvider dataProvider;
/**
* {@link org.valkyriercp.binding.validation.ValidationResultsModel}
* combining results from embedded forms and other messages.
*/
private final DefaultValidationResultsModel validationResultsModel = new DefaultValidationResultsModel();
private ListRetrievingWorker listWorker;
private final MaximumRowsExceededMessage maximumRowsExceededMessage = new MaximumRowsExceededMessage();
protected static class MaximumRowsExceededMessage extends
DefaultValidationMessage {
private String message;
public MaximumRowsExceededMessage() {
super("maximumRowsExceeded", Severity.WARNING,
"maximumRowsExceeded");
}
@Override
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
/**
* {@link SwingWorker} which retrieves list from
* back-end and fills table with result.
* <p/>
* Remember to set criteria and launch this class in a synchronised block.
*/
private class ListRetrievingWorker extends
SwingWorker<List<Object>, String> {
/**
* The filter criteria to use.
*/
protected Object filterCriteria;
/**
* Additional parameters that are coupled with this instance/run.
*/
protected Map<String, Object> parameters;
@Override
protected List<Object> doInBackground() throws Exception {
return getDataProvider().getList(filterCriteria);
}
/**
* Set the rows in the table.
*/
@Override
protected void done() {
try {
listWorkerDone(get(), parameters);
} catch (InterruptedException e) {
// someone cancelled the retrieval?
} catch (ExecutionException e) {
if (e.getCause() instanceof MaximumRowsExceededException) {
MaximumRowsExceededException mre = (MaximumRowsExceededException) e
.getCause();
setRows(Collections.EMPTY_LIST);
validationResultsModel
.removeMessage(maximumRowsExceededMessage);
maximumRowsExceededMessage.setMessage(getApplicationConfig()
.messageResolver().getMessage(
"dataeditor.maximumRowsExceededException.notice",
new Object[] { mre.getNumberOfRows(),
mre.getMaxRows() }));
validationResultsModel
.addMessage(maximumRowsExceededMessage);
if (getToggleFilterCommand() != null) {
getToggleFilterCommand().doShow();
}
} else {
throw new RuntimeException(e);
}
} finally {
getApplicationConfig().windowManager().getActiveWindow()
.getStatusBar().getProgressMonitor().done();
// getFilterForm().getCommitCommand().setEnabled(true);
// getRefreshCommand().setEnabled(true);
listWorker = null;
}
}
}
/**
* This method is called on the gui-thread when the worker ends. As default
* it will check for the PARAMETER_DEFAULT_SELECTED_OBJECT parameter in the
* map.
*
* @param rows
* fetched by the listWorker.
* @param parameters
* a map of parameters specific to this listWorker instance.
*/
protected void listWorkerDone(List<Object> rows,
Map<String, Object> parameters) {
setRows(rows);
// remove maximumRowsExceededMessages if needed
validationResultsModel.removeMessage(maximumRowsExceededMessage);
if ((rows == null) || (rows.size() == 0)) {
return;
}
Object defaultSelectedObject = null;
if (parameters.containsKey(PARAMETER_DEFAULT_SELECTED_OBJECT)) {
defaultSelectedObject = parameters
.get(PARAMETER_DEFAULT_SELECTED_OBJECT);
}
if (defaultSelectedObject == null) {
getTableWidget().selectRowObject(0, null);
} else {
getTableWidget().selectRowObject(defaultSelectedObject, null);
}
}
/**
* Default constructor. Add id, {@link DataProvider},
* {@link org.valkyriercp.form.Form}s and listView later.
*
* @see #setDataProvider(DataProvider)
* @see #setDetailForm(AbstractForm)
* @see #setFilterForm(FilterForm)
* @see #setId(String)
* @see #setTableWidget(TableDescription)
*/
public DefaultDataEditorWidget(String id) {
setId(id);
}
// /**
// * Constructor with id and {@link DataProvider}. Add {@link
// org.valkyriercp.form.Form}s and listView later.
// *
// * @param id used to fetch messages/icons.
// * @param provider provides the data manipulation and possible CRUD
// options.
// * @see #setDetailForm(AbstractForm)
// * @see #setFilterForm(FilterForm)
// * @see #setTableWidget(TableDescription)
// * @see #setTableWidget(TableWidget)
// */
// public DefaultDataEditorWidget(String id, DataProvider provider)
// {
// this(id, provider, null, null, null);
// }
//
// public DefaultDataEditorWidget(DataProvider provider, AbstractForm form,
// TableDescription tableDesc,
// FilterForm filterForm)
// {
// this(null, provider, form, tableDesc, filterForm);
// }
//
// /**
// * Constructor allowing to set all major components at once.
// *
// * @param id used to fetch messages/icons.
// * @param provider provides the data manipulation and possible CRUD
// options.
// * @param form used to display and edit one row detail.
// * @param tableDesc describes the columns of the table to build.
// * @param filterForm optional form used to filter the data.
// */
// public DefaultDataEditorWidget(String id, DataProvider provider,
// AbstractForm form,
// TableDescription tableDesc, FilterForm filterForm)
// {
// setId(id);
// setDataProvider(provider);
// setDetailForm(form);
// setTableWidget(tableDesc);
// setFilterForm(filterForm);
// }
@Override
public void setTitle(String title) {
super.setTitle(title);
getTableWidget().getTable().setName(title);
}
/**
* Returns only the detail form widget
*/
@Override
public Widget createDetailWidget() {
return new AbstractWidget() {
@Override
public void onAboutToShow() {
DefaultDataEditorWidget.this.onAboutToShow();
}
@Override
public void onAboutToHide() {
DefaultDataEditorWidget.this.onAboutToHide();
}
public JComponent getComponent() {
return getDetailForm().getControl();
}
@Override
public List<? extends AbstractCommand> getCommands() {
return Arrays.asList(getDetailForm().getCommitCommand());
}
@Override
public String getId() {
return DefaultDataEditorWidget.this.getId() + "."
+ getDetailForm().getId();
}
};
}
/**
* Set the form that will handle one detail item.
*/
protected void setDetailForm(AbstractForm detailForm) {
if (this.detailForm != null) {
validationResultsModel.remove(this.detailForm.getFormModel()
.getValidationResults());
}
this.detailForm = detailForm;
if (this.detailForm != null) {
validationResultsModel.add(this.detailForm.getFormModel()
.getValidationResults());
}
}
@Override
public AbstractForm getDetailForm() {
return this.detailForm;
}
/**
* Set the form to use as filter.
*
* @see DataProvider#supportsFiltering()
*/
protected void setFilterForm(FilterForm filterForm) {
if (this.filterForm != null) {
validationResultsModel.remove(this.filterForm.getFormModel()
.getValidationResults());
}
this.filterForm = filterForm;
if (this.filterForm != null) {
validationResultsModel.add(filterForm.getFormModel()
.getValidationResults());
}
}
@Override
public FilterForm getFilterForm() {
return this.filterForm;
}
public void setFilterModel(Object model) {
getFilterForm().setFormObject(model);
}
/**
* Create a {@link GlazedListTableWidget} based on the given
* {@link TableDescription} to be used as listView.
*
* @param tableDescription
* description of columns used to create the table.
*/
protected void setTableWidget(final TableDescription tableDescription) {
Assert.notNull(tableDescription);
tableWidget = new CachedCallable<TableWidget>() {
@Override
protected TableWidget doCall() {
TableWidget tableWidget = new GlazedListTableWidget(null,
tableDescription);
tableWidget.addSelectionObserver(tableSelectionObserver);
return tableWidget;
}
};
}
// /**
// * Set the listView of this dataEditor.
// */
// protected void setTableWidget(TableWidget tableWidget)
// {
// if (this.tableWidget != null)
// {
// this.tableWidget.removeSelectionObserver(tableSelectionObserver);
// }
//
// this.tableWidget = tableWidget;
//
// if (this.tableWidget != null)
// {
// this.tableWidget.addSelectionObserver(tableSelectionObserver);
// }
// }
@Override
public TableWidget getTableWidget() {
return this.tableWidget.safeCall();
}
/**
* Set the provider to use for data manipulation.
*/
protected void setDataProvider(DataProvider provider) {
if ((this.dataProvider != null)
&& (this.dataProvider.getRefreshPolicy() == DataProvider.RefreshPolicy.ON_USER_SWITCH)) {
getApplicationConfig()
.applicationSession()
.removePropertyChangeListener(ApplicationSession.USER, this);
}
this.dataProvider = provider;
if ((this.dataProvider != null)
&& (this.dataProvider.getRefreshPolicy() == DataProvider.RefreshPolicy.ON_USER_SWITCH)) {
getApplicationConfig().applicationSession().addPropertyChangeListener(
ApplicationSession.USER, this);
}
}
public DataProvider getDataProvider() {
return dataProvider;
}
@Override
protected boolean isUpdateRowSupported() {
return this.dataProvider.supportsUpdate();
}
@Override
protected boolean isAddRowSupported() {
return this.dataProvider.supportsCreate();
}
@Override
protected boolean isCloneRowSupported() {
return this.dataProvider.supportsClone()
&& this.dataProvider.supportsCreate();
}
@Override
protected boolean isFilterSupported() {
return this.dataProvider.supportsFiltering();
}
@Override
protected boolean isRemoveRowsSupported() {
return this.dataProvider.supportsDelete();
}
/**
* Executes filter and fills table in specific manner:
* <p/>
* <ul>
* <li>set baseCriteria if needed</li>
* <li>set searchCriteria on filterForm</li>
* <li>set searchCriteria on worker</li>
* <li>pass parameter map to worker</li>
* <li>launch worker to retrieve list from back-end and fill table</li>
* <li>when done, set list and execute additional code taking the parameters
* into account</li>
* </ul>
*
* @param parameters
* a number of parameters that can influence this run. Should be
* a non-modifiable map or a specific instance.
*/
@Override
public synchronized void executeFilter(Map<String, Object> parameters) {
if (listWorker == null) {
if (dataProvider.supportsBaseCriteria()) {
dataProvider.setBaseCriteria(getBaseCriteria());
}
StatusBar statusBar = getApplicationConfig().windowManager()
.getActiveWindow().getStatusBar();
statusBar.getProgressMonitor().taskStarted(
getApplicationConfig().messageResolver().getMessage("statusBar",
"loadTable", MessageConstants.LABEL),
StatusBarProgressMonitor.UNKNOWN);
// getFilterForm().getCommitCommand().setEnabled(false);
// getRefreshCommand().setEnabled(false);
listWorker = new ListRetrievingWorker();
if (dataProvider.supportsFiltering()) {
if (parameters.containsKey(PARAMETER_FILTER)) {
setFilterModel(parameters.get(PARAMETER_FILTER));
}
listWorker.filterCriteria = getFilterForm().getFilterCriteria();
}
listWorker.parameters = parameters;
log.debug("Execute Filter with criteria: "
+ listWorker.filterCriteria + " and parameters: "
+ parameters);
listWorker.execute();
}
}
/**
* @see #executeFilter(Map)
*/
@Override
public void executeFilter() {
executeFilter(Collections.EMPTY_MAP);
}
/**
* <b>Warning!</b> this can block threads for an extended period, make sure
* you're aware of this.
* <p/>
* <p>
* Alternative: {@link #executeFilter()} will launch separate worker and
* fills table.
* </p>
*
* @param criteria
* @return
*/
protected List getList(Object criteria) {
if (this.dataProvider.supportsBaseCriteria()) {
this.dataProvider.setBaseCriteria(getBaseCriteria());
}
try {
List dataSet = this.dataProvider.getList(criteria);
setRows(dataSet);
setMessage(null);
return dataSet;
} catch (MaximumRowsExceededException mre) {
setRows(Collections.EMPTY_LIST);
setMessage(new DefaultMessage(getApplicationConfig().messageResolver()
.getMessage(
"dataeditor.maximumRowsExceededException.notice",
new Object[] { mre.getNumberOfRows(),
mre.getMaxRows() }), Severity.WARNING));
if (getToggleFilterCommand() != null) {
getToggleFilterCommand().doShow();
}
return null;
}
}
/**
* Internal fill method of the datatable
* <p/>
* WARNING: not threadsafe, please call me on the EDT!
*/
protected void setRows(List dataSet) {
getTableWidget().setRows(dataSet);
}
protected Object getBaseCriteria() {
return null;
}
@Override
protected Object loadEntityDetails(Object baseObject, boolean forceLoad) {
return this.dataProvider.getDetailObject(baseObject, forceLoad);
}
public Object loadSimpleEntity(Object baseObject) {
Object returnValue = this.dataProvider.getSimpleObject(baseObject);
if (returnValue == null) {
throw new NullPointerException(
"Returnvalue for dataprovider simple was null");
}
return returnValue;
}
@Override
protected Object saveEntity(Object dirtyObject) {
if (!this.dataProvider.supportsUpdate()) {
return null;
}
return this.dataProvider.update(dirtyObject);
}
@Override
protected void newRow(Object newClone) {
if (newClone == null) {
super.newRow(getDataProvider().newInstance(
getFilterForm() == null ? null : getFilterForm()
.getFormObject()));
} else {
super.newRow(newClone);
}
}
@Override
protected Object createNewEntity(Object newObject) {
if (!this.dataProvider.supportsCreate()) {
return null;
}
return this.dataProvider.create(newObject);
}
@Override
protected Object cloneEntity(Object sampleObject) {
if (!this.dataProvider.supportsClone()) {
return null;
}
return this.dataProvider.clone(sampleObject);
}
@Override
protected void removeEntity(Object objectToRemove) {
if (!this.dataProvider.supportsDelete()) {
return;
}
this.dataProvider.delete(objectToRemove);
}
public void update(Observable o, Object arg) {
if (arg instanceof DataProviderEvent) {
DataProviderEvent obsAct = (DataProviderEvent) arg;
int act = obsAct.getEventType();
if (act == DataProviderEvent.EVENT_TYPE_NEW) {
this.getTableWidget().addRowObject(obsAct.getNewEntity());
} else if (act == DataProviderEvent.EVENT_TYPE_UPDATE) {
replaceRowObject(obsAct.getOldEntity(), obsAct.getNewEntity());
} else if (act == DataProviderEvent.EVENT_TYPE_DELETE) {
this.getTableWidget().removeRowObject(obsAct.getOldEntity());
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void onAboutToShow() {
log.debug(getId() + ": onAboutToShow with refreshPolicy: "
+ dataProvider.getRefreshPolicy());
super.onAboutToShow();
dataProvider.addDataProviderListener(this);
registerListeners();
if (detailForm instanceof Widget) {
((Widget) detailForm).onAboutToShow();
}
getTableWidget().onAboutToShow();
// lazy loading, if no list is present, load when widget is shown
// include RefreshPolicy given by DataProvider
if ((dataProvider.getRefreshPolicy() != DataProvider.RefreshPolicy.NEVER)
&& (getTableWidget().isEmpty())) {
executeFilter();
} else if (!getTableWidget().hasSelection()) {
getTableWidget().selectRowObject(0, this);
}
}
/**
* {@inheritDoc}
*/
@Override
public void onAboutToHide() {
log.debug(getId() + ": onAboutToHide with refreshPolicy: "
+ dataProvider.getRefreshPolicy());
super.onAboutToHide();
this.dataProvider.removeDataProviderListener(this);
unRegisterListeners();
if (detailForm instanceof Widget) {
((Widget) detailForm).onAboutToHide();
}
if (dataProvider.getRefreshPolicy() == DataProvider.RefreshPolicy.ALLWAYS) {
getTableWidget().setRows(Collections.EMPTY_LIST);
}
}
protected void registerListeners() {
}
protected void unRegisterListeners() {
}
/**
* note: differs from previous method to allow setting of formObject on
* filterForm. Will probably end up in refactoring of dataeditorwidget.
*
* @param criteria
* formObject to set on FilterForm.
* @return
*/
public Object setSelectedSearch(Object criteria) {
// filterField leegmaken
if (getTableWidget().getTextFilterField() != null) {
getTableWidget().getTextFilterField().setText("");
}
// if Referable == null, empty filterForm and execute filter
if (criteria == null) {
if (dataProvider.supportsFiltering()) {
getFilterForm().getNewFormObjectCommand().execute();
}
executeFilter();
return null;
}
List resultList = getList(criteria);
if (dataProvider.supportsFiltering()) {
// adapt filterForm to reflect referable criteria
if ((resultList == null) || (resultList.size() > 0)) // fill in
// referable
{
getFilterForm().setFormObject(criteria);
} else { // empty filterForm and execute
getFilterForm().getNewFormObjectCommand().execute();
executeFilter();
}
}
if (resultList != null && resultList.size() == 1) {
// return the detailObject
// return loadEntityDetails(resultList.get(0));
return loadSimpleEntity(resultList.get(0));
}
return resultList;
}
public void refreshSelectedObject() {
Object selected = getSelectedRowObject();
getTableWidget().selectRowObject(-1, null);
getTableWidget().selectRowObject(selected, null);
}
public void propertyChange(PropertyChangeEvent evt) {
if ((dataProvider.getRefreshPolicy() == DataProvider.RefreshPolicy.ALLWAYS)
|| (dataProvider.getRefreshPolicy() == DataProvider.RefreshPolicy.ON_USER_SWITCH)) {
log.debug("USER changed event, refreshPolicy= "
+ dataProvider.getRefreshPolicy());
if ((evt.getNewValue() == null)) {
setRows(Collections.EMPTY_LIST);
} else if (isShowing()) {
executeFilter();
}
}
}
@Override
protected final DefaultValidationResultsModel getValidationResults() {
return validationResultsModel;
}
}