/*
* RHQ Management Platform
* Copyright 2010, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.coregui.client.components.form;
import java.util.EnumSet;
import java.util.List;
import com.smartgwt.client.data.Criteria;
import com.smartgwt.client.data.DSCallback;
import com.smartgwt.client.data.DSRequest;
import com.smartgwt.client.data.DSResponse;
import com.smartgwt.client.data.Record;
import com.smartgwt.client.rpc.RPCResponse;
import com.smartgwt.client.types.DSOperationType;
import com.smartgwt.client.types.Overflow;
import com.smartgwt.client.types.VerticalAlignment;
import com.smartgwt.client.widgets.Canvas;
import com.smartgwt.client.widgets.IButton;
import com.smartgwt.client.widgets.Label;
import com.smartgwt.client.widgets.events.ClickEvent;
import com.smartgwt.client.widgets.events.ClickHandler;
import com.smartgwt.client.widgets.form.events.ItemChangedEvent;
import com.smartgwt.client.widgets.form.events.ItemChangedHandler;
import com.smartgwt.client.widgets.form.fields.FormItem;
import com.smartgwt.client.widgets.grid.ListGridRecord;
import org.rhq.coregui.client.BookmarkableView;
import org.rhq.coregui.client.CoreGUI;
import org.rhq.coregui.client.DetailsView;
import org.rhq.coregui.client.ViewPath;
import org.rhq.coregui.client.components.TitleBar;
import org.rhq.coregui.client.util.Log;
import org.rhq.coregui.client.util.RPCDataSource;
import org.rhq.coregui.client.util.enhanced.EnhancedHLayout;
import org.rhq.coregui.client.util.enhanced.EnhancedIButton;
import org.rhq.coregui.client.util.enhanced.EnhancedIButton.ButtonColor;
import org.rhq.coregui.client.util.enhanced.EnhancedToolStrip;
import org.rhq.coregui.client.util.enhanced.EnhancedVLayout;
import org.rhq.coregui.client.util.message.Message;
/**
* An editor for a SmartGWT {@link Record} backed by an {@link RPCDataSource}.
*
* @author Ian Springer
*/
@SuppressWarnings("unchecked")
public abstract class AbstractRecordEditor<DS extends RPCDataSource> extends EnhancedVLayout implements
BookmarkableView, DetailsView {
private static final Label LOADING_LABEL = new Label(MSG.common_msg_loading());
private static final String FIELD_ID = "id";
private static final String FIELD_NAME = "name";
/**
* this field can be send as {@link DSRequest} attribute to {@link #save(DSRequest)} method to non-null value to
* prevent refreshing all views in case {@link #save(DSRequest)} succeeds
*/
protected static final String FIELD_NO_REFRESH = "!no-refresh";
private static final int ID_NEW = 0;
private int recordId;
private TitleBar titleBar;
private EnhancedDynamicForm form;
private DS dataSource;
private boolean isReadOnly;
private String dataTypeName;
private String listViewPath;
private ButtonBar buttonBar;
private EnhancedVLayout contentPane;
private boolean postFetchHandlerExecutedAlready;
public AbstractRecordEditor(DS dataSource, int recordId, String dataTypeName, String headerIcon) {
super();
this.dataSource = dataSource;
this.recordId = recordId;
this.dataTypeName = capitalize(dataTypeName);
setLayoutMargin(0);
setMembersMargin(16);
// Display a "Loading..." label at the top of the view to keep the user informed.
addMember(LOADING_LABEL);
// Add title bar. We'll set the actual title later.
this.titleBar = new TitleBar(null, headerIcon);
this.titleBar.hide();
addMember(this.titleBar);
}
@Override
public void renderView(ViewPath viewPath) {
// TODO: The below line is temporary until TableSection.renderView() advances the view id pointer as it should.
viewPath.next();
String parentViewPath = viewPath.getParentViewPath();
this.listViewPath = parentViewPath; // e.g. Administration/Security/Roles
}
/**
* <b>IMPORTANT:</b> Subclasses are responsible for invoking this method after all asynchronous operations invoked
* by {@link #renderView(ViewPath)} have completed.
*
* @param isReadOnly whether or not the record editor should be in read-only mode
*/
protected void init(boolean isReadOnly) {
if (this.recordId == ID_NEW && isReadOnly) {
Message message = new Message(MSG.widget_recordEditor_error_permissionCreate(this.dataTypeName),
Message.Severity.Error);
CoreGUI.goToView(getListViewPath(), message);
} else {
this.isReadOnly = isReadOnly;
this.contentPane = buildContentPane();
this.contentPane.hide();
addMember(this.contentPane);
this.buttonBar = buildButtonBar();
if (this.buttonBar != null) {
this.buttonBar.hide();
addMember(this.buttonBar);
}
if (this.recordId == ID_NEW) {
editNewRecord();
// Now that all the widgets have been created and initialized, make everything visible.
displayForm();
} else {
fetchExistingRecord(this.recordId);
}
}
}
protected EnhancedVLayout buildContentPane() {
EnhancedVLayout contentPane = new EnhancedVLayout();
contentPane.setWidth100();
contentPane.setHeight100();
contentPane.setOverflow(Overflow.AUTO);
//contentPane.setPadding(7);
this.form = buildForm();
contentPane.addMember(this.form);
return contentPane;
}
protected ButtonBar buildButtonBar() {
if (this.isReadOnly) {
return null;
}
return new ButtonBar();
}
protected EnhancedDynamicForm buildForm() {
boolean isNewRecord = (this.recordId == ID_NEW);
EnhancedDynamicForm form = new EnhancedDynamicForm(isFormReadOnly(), isNewRecord);
form.setDataSource(this.dataSource);
List<FormItem> items = createFormItems(form);
form.setFields(items.toArray(new FormItem[items.size()]));
form.addItemChangedHandler(new ItemChangedHandler() {
public void onItemChanged(ItemChangedEvent event) {
AbstractRecordEditor.this.onItemChanged();
}
});
return form;
}
protected boolean isFormReadOnly() {
return this.isReadOnly;
}
public EnhancedVLayout getContentPane() {
return this.contentPane;
}
public void setForm(EnhancedDynamicForm form) {
this.form = form;
}
public EnhancedDynamicForm getForm() {
return this.form;
}
public DS getDataSource() {
return this.dataSource;
}
public String getDataTypeName() {
return dataTypeName;
}
public boolean isReadOnly() {
return this.isReadOnly;
}
public int getRecordId() {
return this.recordId;
}
public boolean isNewRecord() {
return (getRecordId() == ID_NEW);
}
public String getListViewPath() {
return this.listViewPath;
}
protected abstract List<FormItem> createFormItems(EnhancedDynamicForm form);
/**
* This method should be called whenever any editable item on the page is changed. It will enable the Reset button
* and update the Save button's enablement based on whether or not all items on the form are valid.
*/
public void onItemChanged() {
// If we're in editable mode, update the button enablement.
if (!this.isReadOnly) {
IButton saveButton = this.buttonBar.getSaveButton();
if (saveButton.isDisabled()) {
saveButton.setDisabled(false);
}
if (showResetButton()) {
IButton resetButton = this.buttonBar.getResetButton();
if (resetButton.isDisabled()) {
resetButton.setDisabled(false);
}
}
}
}
protected boolean showResetButton() {
return true;
}
protected void reset() {
this.form.resetValues();
}
protected void postSaveAction() {
// do nothing in the default implementation, override this method if needed
}
protected void save(final DSRequest requestProperties) {
if (!this.form.validate()) {
Message message = new Message(MSG.widget_recordEditor_warn_validation(this.dataTypeName),
Message.Severity.Warning, EnumSet.of(Message.Option.Transient));
CoreGUI.getMessageCenter().notify(message);
return;
}
this.form.saveData(new DSCallback() {
public void execute(DSResponse response, Object rawData, DSRequest request) {
if (response.getStatus() == RPCResponse.STATUS_SUCCESS) {
Record[] data = response.getData();
Record record = data[0];
String id = record.getAttribute(FIELD_ID);
String name = record.getAttribute(getTitleFieldName());
Message message;
String conciseMessage;
String detailedMessage;
DSOperationType operationType = request.getOperationType();
if (Log.isDebugEnabled()) {
Object dataObject = dataSource.copyValues(record);
if (operationType == DSOperationType.ADD) {
Log.debug("Created: " + dataObject);
} else {
Log.debug("Updated: " + dataObject);
}
}
switch (operationType) {
case ADD:
conciseMessage = MSG.widget_recordEditor_info_recordCreatedConcise(dataTypeName);
detailedMessage = MSG.widget_recordEditor_info_recordCreatedDetailed(dataTypeName, name);
if (CoreGUI.isDebugMode()) {
conciseMessage += " (" + FIELD_ID + "=" + id + ")";
detailedMessage += " (" + FIELD_ID + "=" + id + ")";
}
break;
case UPDATE:
conciseMessage = MSG.widget_recordEditor_info_recordUpdatedConcise(dataTypeName);
detailedMessage = MSG.widget_recordEditor_info_recordUpdatedDetailed(dataTypeName, name);
break;
default:
throw new IllegalStateException(MSG
.widget_recordEditor_error_unsupportedOperationType(operationType.name()));
}
message = new Message(conciseMessage, detailedMessage);
// only refresh if no-refresh attribute is missing
boolean refresh = requestProperties.getAttribute(FIELD_NO_REFRESH) == null;
postSaveAction();
CoreGUI.goToView(getListViewPath(), message, refresh);
} else if (response.getStatus() == RPCResponse.STATUS_VALIDATION_ERROR) {
String causes = null;
if (response.getErrors() != null && !response.getErrors().isEmpty()) {
// prepare detailed error message
StringBuffer sb = new StringBuffer();
for (Object cause : response.getErrors().values()) {
sb.append(cause);
sb.append('\n');
}
causes = sb.toString();
}
Message message = new Message(MSG.widget_recordEditor_error_operationInvalidValues(), causes,
Message.Severity.Error);
CoreGUI.getMessageCenter().notify(message);
} else {
// assume failure
Message message = new Message(MSG.widget_recordEditor_error_operation(), Message.Severity.Error);
CoreGUI.getMessageCenter().notify(message);
}
}
}, requestProperties);
}
protected void editNewRecord() {
// Update the view title.
this.titleBar.setTitle(MSG.widget_recordEditor_title_new(this.dataTypeName));
// Create a new record.
Record record = createNewRecord();
// And populate the form with it.
this.form.editRecord(record);
this.form.setSaveOperationType(DSOperationType.ADD);
// But make sure the value of the "id" field is set to "0", since a value of null could cause the dataSource's
// copyValues(Record) impl to choke.
FormItem idItem = this.form.getItem(FIELD_ID);
if (idItem != null) {
idItem.setDefaultValue(ID_NEW);
idItem.hide();
}
editRecord(record);
}
protected void editExistingRecord(Record record) {
// Update the view title.
String recordName = record.getAttribute(getTitleFieldName());
String title = (this.isReadOnly) ? MSG.widget_recordEditor_title_view(this.dataTypeName, recordName) : MSG
.widget_recordEditor_title_edit(this.dataTypeName, recordName);
this.titleBar.setTitle(title);
// Load the data into the form.
this.form.editRecord(record);
// Perform up front validation for existing records.
// NOTE: We do *not* do this for new records, since we expect most of the required fields to be blank.
this.form.validate();
editRecord(record);
}
/**
* Initialize the editor with the data from the passed record. This method will be called for both new records
* (after the record has been created by {@link #createNewRecord()}) and existing records (after the record has
* been fetched by {@link #fetchExistingRecord(int)}.
*
* @param record the record
*/
protected void editRecord(Record record) {
}
// Subclasses will generally want to override this.
protected Record createNewRecord() {
return new ListGridRecord();
}
private void displayForm() {
removeMember(LOADING_LABEL);
LOADING_LABEL.destroy();
for (Canvas member : getMembers()) {
member.show();
}
markForRedraw();
}
protected void fetchExistingRecord(final int recordId) {
Criteria criteria = new Criteria();
criteria.addCriteria(FIELD_ID, recordId);
this.form.fetchData(criteria, new DSCallback() {
public void execute(DSResponse response, Object rawData, DSRequest request) {
// The below check is a workaround for a SmartGWT bug, where it calls the execute() method on this
// callback twice, rather than once.
// TODO: Remove it once the SmartGWT bug has been fixed.
if (!postFetchHandlerExecutedAlready) {
postFetchHandlerExecutedAlready = true;
if (response.getStatus() == DSResponse.STATUS_SUCCESS) {
Record[] records = response.getData();
if (records.length == 0) {
throw new IllegalStateException(MSG.widget_recordEditor_error_noRecords());
}
if (records.length > 1) {
throw new IllegalStateException(MSG.widget_recordEditor_error_multipleRecords());
}
Record record = records[0];
editExistingRecord(record);
// Now that all the widgets have been created and initialized, make everything visible.
displayForm();
}
}
}
});
}
protected String getTitleFieldName() {
return FIELD_NAME;
}
@Override
public boolean isEditable() {
return (!this.isReadOnly);
}
protected static ListGridRecord[] toListGridRecordArray(Record[] roleRecords) {
ListGridRecord[] roleListGridRecords = new ListGridRecord[roleRecords.length];
for (int i = ID_NEW, roleRecordsLength = roleRecords.length; i < roleRecordsLength; i++) {
Record roleRecord = roleRecords[i];
roleListGridRecords[i] = (ListGridRecord) roleRecord;
}
return roleListGridRecords;
}
private static String capitalize(String itemTitle) {
return Character.toUpperCase(itemTitle.charAt(ID_NEW)) + itemTitle.substring(1);
}
protected class ButtonBar extends EnhancedToolStrip {
private IButton saveButton;
private IButton resetButton;
private IButton cancelButton;
ButtonBar() {
super();
setWidth100();
setHeight(35);
EnhancedVLayout vLayout = new EnhancedVLayout();
vLayout.setAlign(VerticalAlignment.CENTER);
vLayout.setLayoutMargin(4);
EnhancedHLayout hLayout = new EnhancedHLayout();
hLayout.setMembersMargin(10);
vLayout.addMember(hLayout);
saveButton = new EnhancedIButton(MSG.common_button_save(), ButtonColor.BLUE);
saveButton.setDisabled(true);
saveButton.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent clickEvent) {
save(new DSRequest());
}
});
hLayout.addMember(saveButton);
if (showResetButton()) {
resetButton = new EnhancedIButton(MSG.common_button_reset());
resetButton.setDisabled(true);
resetButton.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent clickEvent) {
resetButton.disable();
saveButton.disable();
reset();
}
});
hLayout.addMember(resetButton);
}
cancelButton = new EnhancedIButton(MSG.common_button_cancel());
cancelButton.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent clickEvent) {
CoreGUI.goToView(getListViewPath());
}
});
hLayout.addMember(cancelButton);
addMember(vLayout);
}
public IButton getCancelButton() {
return cancelButton;
}
public IButton getResetButton() {
return resetButton;
}
public IButton getSaveButton() {
return saveButton;
}
}
}