CRUD with GWT, gwt-platform, smartGWT and JPA
In the previous post we learnt about a variation of the Model-View-Presenter (MVP) design pattern. In this post, we're going to look at how to implement the four basic functions of persistent storage when working with GWT, gwt-platform's MVP and Dispatch components, smartGWT and JPA.
In this post, we'll:
- Create an Account entity
- Retrieve an Account entity
- Update an Account entity
- Delete an Account entity
- Test the application in development mode
Note: You can download the sample application from the CRMdipity (Se-r-en-dipity) project's download page.
1. Create an Account entity
We can create a new Account by clicking the New button on Serendiptiy's Main page:

First, we need to register a handler for the New button in the nested Accounts View:
Note: The initToolBar method is called by the Accounts View's bindCustomUiHandler method.
...
protected void initToolBar() {
toolBar.addButton(ToolBar.NEW_BUTTON, Serendipity.getConstants().NewButton(),
Serendipity.getConstants().NewButtonTooltip(), new ClickHandler() {
public void onClick(ClickEvent event) {
if (getUiHandlers() != null) {
getUiHandlers().onNewButtonClicked();
}}
}
);
...
}
We also we need to define a UI handler in the nested Accounts Presenter:
...
private static final String HOST_FILENAME = "Account.html";
private static final String ACTIVITY = "activity";
private static final String NEW = "new";
private static final String EDIT = "edit";
private static final String PARAMETER_SEPERATOR = "&"; // GWTP "?"
private static final String NAME = "_blank";
private static final String FEATURES = "width=760, height=480";
public void onNewButtonClicked() {
StringBuilder url = new StringBuilder();
url.append(HOST_FILENAME).append("?");
url.append(AccountsRecord.ACCOUNT_ID).append("=").append("0").append(PARAMETER_SEPERATOR);
url.append(ACTIVITY).append("=").append(NEW);
Window.open(getRelativeURL(url.toString()), NAME, FEATURES);
}
Take a look at the Accounts Presenter's onNewButtonClicked method and you will notice that it calls Window.open. This means that Serendipity needs to be able to manage multiple host pages and thanks to GWT Multipage, this can be acheived by simply adding an annotation to the Account entry point class:
...
@UrlPatternEntryPoint(value = "(Account.html)?(\\\\?accountId=[0-9]+&activity=(new|edit))?((&|\\\\?)locale=(en|de))?")
public class AccountEntryPoint implements EntryPoint {
...
Now, when you click the
New button on the Accounts ToolBar, the Account window will be displayed:

Fill in the form and click the
Save and Close button:

As we did with the Accounts View, we need to register a handler in the AccountInformation View:
...
toolBar.addButton(ToolBar.SAVE_AND_CLOSE_BUTTON,
Serendipity.getConstants().SaveAndCloseButton(),
Serendipity.getConstants().SaveAndCloseButtonTooltip(), new ClickHandler() {
public void onClick(ClickEvent event) {
if (getUiHandlers() != null) {
getUiHandlers().onSaveAndCloseButtonClicked(getFields());
}}
}
);
...
}
And, we need to define a UI handler in the AccountInformation Presenter:
...
public void onSaveAndCloseButtonClicked(AccountDto accountDto) {
createOrUpdateAccount(accountDto);
close();
}
native public static void close() /*-{ $wnd.close(); }-*/;
public void createOrUpdateAccount(AccountDto accountDto) {
if (accountDto.getAccountId() == -1 ) {
createAccount(accountDto);
}
else {
updateAccount(accountDto);
}
}
public void createAccount(AccountDto accountDto) {
dispatcher.execute(new CreateAccountAction(accountDto),
new AsyncCallback<CreateAccountResult>() {
@Override
public void onFailure(Throwable caught) {
Log.debug("onFailure() - " + caught.getMessage());
}
@Override
public void onSuccess(CreateAccountResult result) {
Log.debug("onSuccess() - accountId: " + result.getAccountId());
getView().setAccountId(result.getAccountId());
}
});
}
We can use gwt-platform's @GenDispatch annotation to generate the 'CreateAccount' Action and Result classes:
package au.com.uptick.serendipity.shared.action;
import au.com.uptick.serendipity.shared.dto.sales.AccountDto;
import com.gwtplatform.annotation.GenDispatch;
import com.gwtplatform.annotation.In;
import com.gwtplatform.annotation.Out;
import com.gwtplatform.dispatch.shared.UnsecuredActionImpl;
@GenDispatch(isSecure = false, serviceName = UnsecuredActionImpl.DEFAULT_SERVICE_NAME)
public class CreateAccount {
@In(1) AccountDto accountDto;
@Out(1) Long accountId;;
}
However, we'll need to provide our own CreateAccount Handler implementation:
...
public class CreateAccountHandler implements
ActionHandler<CreateAccountAction, CreateAccountResult> {
private final Provider<HttpServletRequest> requestProvider;
private final ServletContext servletContext;
@Inject
CreateAccountHandler(final ServletContext servletContext,
final Provider<HttpServletRequest> requestProvider) {
this.servletContext = servletContext;
this.requestProvider = requestProvider;
}
@Override
public CreateAccountResult execute(final CreateAccountAction action,
final ExecutionContext context) throws ActionException {
CreateAccountResult result = null;
AccountDao accountDao = new AccountDao();
DOMConfigurator.configure("log4j.xml");
try {
Long accountId= accountDao.createAccount(createAccount(action.getAccountDto()))
result = new CreateAccountResult(accountId);
}
catch (Exception e) {
throw new ActionException(e);
}
return result;
}
private Account createAccount(AccountDto accountDto) {
Account account = new Account();
account.setAccountName(accountDto.getAccountName());
account.setAccountNumber(accountDto.getAccountNumber());
// Address Information
List<AddressDto> addressDtos = accountDto.getAddresses();
if (addressDtos != null) {
List<Address> addresses = new ArrayList<Address>(addressDtos.size());
for (AddressDto addressDto : addressDtos) {
addresses.add(createAddress(addressDto));
}
account.setAddresses(addresses);
}
return account;
}
private Address createAddress(AddressDto addressDto) {
return new Address(addressDto.getAddressName(),
addressDto.getAddressLine1(), addressDto.getAddressLine2(), addressDto.getAddressLine3(),
addressDto.getCity(), addressDto.getState(), addressDto.getPostalCode(),
addressDto.getCountry(), addressDto.getAddressType());
}
@Override
public Class<CreateAccountAction> getActionType() {
return CreateAccountAction.class;
}
@Override
public void undo(CreateAccountAction action, CreateAccountResult result,
ExecutionContext context) throws ActionException {
}
}
Click the
Refresh button and the new Account will be displayed in Serendiptiy's Main page:

2. Retrieve an Account entity
We can retrieve an Account by double-clicking on a record in Serendiptiy's Main page. First we need to register a handler in the nested Accounts View:
...
protected void bindCustomUiHandlers() {
// initialise the ToolBar and register its handlers
initToolBar();
// register the ListGird handlers
listGrid.addRecordDoubleClickHandler(new RecordDoubleClickHandler() {
@Override
public void onRecordDoubleClick(RecordDoubleClickEvent event) {
Record record = event.getRecord();
accountId = record.getAttributeAsString(AccountsRecord.ACCOUNT_ID);
if (getUiHandlers() != null) {
getUiHandlers().onRecordDoubleClicked(accountId);
}
}
});
}
We also need to define a UI handler in the nested Accounts Presenter:
...
private static final String HOST_FILENAME = "Account.html";
private static final String ACTIVITY = "activity";
private static final String NEW = "new";
private static final String EDIT = "edit";
private static final String PARAMETER_SEPERATOR = "&"; // GWTP "?"
private static final String NAME = "_blank";
private static final String FEATURES = "width=760, height=480";
public void onRecordDoubleClicked(String accountId) {
StringBuilder url = new StringBuilder();
url.append(HOST_FILENAME).append("?");
url.append(AccountsRecord.ACCOUNT_ID).append("=").append(accountId).append(PARAMETER_SEPERATOR);
url.append(ACTIVITY).append("=").append(EDIT);
Window.open(getRelativeURL(url.toString()), NAME, FEATURES);
}
Take a look at the Accounts Presenter's onRecordDoubleClicked method and the Window.open call, and you will notice that as well as the host file name we also need to include the ACTIVITY ("activity") and the ACCOUNT_ID ("accountId"). These parameters are used in the AccountInformation Presenter's onBind method:
...
@Override
protected void onBind() {
super.onBind();
activity = Window.Location.getParameter(ACTIVITY);
accountId= Window.Location.getParameter(AccountsRecord.ACCOUNT_ID);
if (activity.equals(EDIT)) {
Long id = -1L;
try {
id = Long.valueOf(accountId);
}
catch (NumberFormatException nfe) {
Log.debug("NumberFormatException: " + nfe.getLocalizedMessage());
return;
}
retrieveAccount(id);
}
}
protected void retrieveAccount(Long accountId) {
dispatcher.execute(new RetrieveAccountAction(accountId),
new AsyncCallback<RetrieveAccountResult>() {
@Override
public void onFailure(Throwable caught) {
Log.debug("onFailure() - " + caught.getLocalizedMessage());
}
@Override
public void onSuccess(RetrieveAccountResult result) {
Log.debug("onSuccess()");
getView().setServerResponse(result.getAccountDto());
}
});
}
We can use gwt-platform's @GenDispatch annotation to generate the 'RetrieveAccount' Action and Result classes:
package au.com.uptick.serendipity.shared.action;
import au.com.uptick.serendipity.shared.dto.sales.AccountDto;
import com.gwtplatform.annotation.GenDispatch;
import com.gwtplatform.annotation.In;
import com.gwtplatform.annotation.Out;
import com.gwtplatform.dispatch.shared.UnsecuredActionImpl;
@GenDispatch(isSecure = false, serviceName = UnsecuredActionImpl.DEFAULT_SERVICE_NAME)
public class RetrieveAccount {
@In(1) Long accountId;;
@Out(1) AccountDto accountDto;
}
However, we'll need to provide our own RetrieveAccount Handler implementation:
...
public class RetrieveAccountHandler implements
ActionHandler<RetrieveAccountAction, RetrieveAccountResult> {
private final Provider<HttpServletRequest> requestProvider;
private final ServletContext servletContext;
@Inject
RetrieveAccountHandler(final ServletContext servletContext,
final Provider<HttpServletRequest> requestProvider) {
this.servletContext = servletContext;
this.requestProvider = requestProvider;
}
@Override
public RetrieveAccountResult execute(final RetrieveAccountAction action,
final ExecutionContext context) throws ActionException {
RetrieveAccountResult result = null;
AccountDao accountDao = new AccountDao();
DOMConfigurator.configure("log4j.xml");
try {
Account account = accountDao.retrieveAccount(action.getAccountId());
if (account != null) {
result = new RetrieveAccountResult(createAccountDto(account));
}
}
catch (Exception e) {
Log.warn("Unable to retrieve Account - ", e);
throw new ActionException(e);
}
return result;
}
private AccountDto createAccountDto(Account account) {
AccountDto accountDto = new AccountDto(account.getAccountId());
// General Information
accountDto.setAccountName(account.getAccountName());
accountDto.setAccountNumber(account.getAccountNumber());
// Address Information
List<Address> addresses = account.getAddresses();
if (addresses != null) {
List<AddressDto> addressDtos = new ArrayList<AddressDto>(addresses.size());
for (Address address : addresses) {
addressDtos.add(createAddressDto(address));
}
accountDto.setAddresses(addressDtos);
}
return accountDto;
}
private AddressDto createAddressDto(Address address) {
return new AddressDto(address.getAddressId(), address.getAddressName(),
address.getAddressLine1(), address.getAddressLine2(), address.getAddressLine3(),
address.getCity(), address.getState(), address.getPostalCode(),
address.getCountry(), address.getAddressType());
}
@Override
public Class<RetrieveAccountAction> getActionType() {
return RetrieveAccountAction.class;
}
@Override
public void undo(RetrieveAccountAction action, RetrieveAccountResult result,
ExecutionContext context) throws ActionException {
}
}
Now, when you double-click on a record the Account window will be displayed and it will contain the details for the selected record:

3. Update an Account entity
We can update an Account by double-clicking on a record in Serendiptiy's Main page, updating the Account form and then clicking the
Save and Close button. First, we need to register a handler for the nested AccountInformation View (we did this in 'Create an Account entity', but here's the code again):
...
toolBar.addButton(ToolBar.SAVE_AND_CLOSE_BUTTON,
Serendipity.getConstants().SaveAndCloseButton(),
Serendipity.getConstants().SaveAndCloseButtonTooltip(), new ClickHandler() {
public void onClick(ClickEvent event) {
if (getUiHandlers() != null) {
getUiHandlers().onSaveAndCloseButtonClicked(getFields());
}}
}
);
...
}
And, the AccountInformation Presenter's UI handler:
...
public void onSaveAndCloseButtonClicked(AccountDto accountDto) {
createOrUpdateAccount(accountDto);
close();
}
native public static void close() /*-{ $wnd.close(); }-*/;
public void createOrUpdateAccount(AccountDto accountDto) {
if (accountDto.getAccountId() == -1 ) {
createAccount(accountDto);
}
else {
updateAccount(accountDto);
}
}
public void createAccount(AccountDto accountDto) {
dispatcher.execute(new CreateAccountAction(accountDto),
new AsyncCallback<CreateAccountResult>() {
@Override
public void onFailure(Throwable caught) {
Log.debug("onFailure() - " + caught.getMessage());
}
@Override
public void onSuccess(CreateAccountResult result) {
Log.debug("onSuccess() - accountId: " + result.getAccountId());
getView().setAccountId(result.getAccountId());
}
});
}
public void updateAccount(AccountDto accountDto) {
dispatcher.execute(new UpdateAccountAction(accountDto),
new AsyncCallback<UpdateAccountResult>() {
@Override
public void onFailure(Throwable caught) {
Log.debug("onFailure() - " + caught.getLocalizedMessage());
}
@Override
public void onSuccess(UpdateAccountResult result) {
Log.debug("onSuccess()");
}
});
}
Take a look at the AccountInformation Presenter's onSaveAndCloseButtonClicked method and the createOrUpdateAccount call. The createOrUpdateAccount method checks to see if the Account DTO has a valid Account Identifier and if it does then it calls the updateAccount method. As before, we can use gwt-platform's @GenDispatch annotation to generate the 'UpdateAccount' Action and Result classes:
package au.com.uptick.serendipity.shared.action;
import au.com.uptick.serendipity.shared.dto.sales.AccountDto;
import com.gwtplatform.annotation.GenDispatch;
import com.gwtplatform.annotation.In;
import com.gwtplatform.dispatch.shared.UnsecuredActionImpl;
@GenDispatch(isSecure = false, serviceName = UnsecuredActionImpl.DEFAULT_SERVICE_NAME)
public class UpdateAccount {
@In(1) AccountDto accountDto;
}
However, we'll need to provide our own UpdateAccount Handler implementation:
...
public class UpdateAccountHandler implements
ActionHandler<UpdateAccountAction, UpdateAccountResult> {
private final Provider<HttpServletRequest> requestProvider;
private final ServletContext servletContext;
@Inject
UpdateAccountHandler(final ServletContext servletContext,
final Provider<HttpServletRequest> requestProvider) {
this.servletContext = servletContext;
this.requestProvider = requestProvider;
}
@Override
public UpdateAccountResult execute(final UpdateAccountAction action,
final ExecutionContext context) throws ActionException {
UpdateAccountResult result = null;
AccountDao accountDao = new AccountDao();
DOMConfigurator.configure("log4j.xml");
try {
Account account = accountDao.retrieveAccount(action.getAccountDto().getAccountId());
updateAccount(account, action.getAccountDto());
accountDao.updateAccount(account);
result = new UpdateAccountResult();
}
catch (Exception e) {
Log.warn("Unable to update Account", e);
throw new ActionException(e);
}
return result;
}
private void updateAccount(Account account, AccountDto accountDto) {
account.setAccountName(accountDto.getAccountName());
account.setAccountNumber(accountDto.getAccountNumber());
// get the list of Addresses
List<Address> addresses = account.getAddresses();
if (addresses.size() > 0) {
// Address Information
Iterator<Address> it = addresses.iterator();
Address address = it.next();
// AddressDto Information
List<AddressDto> addressDtos = accountDto.getAddresses();
Iterator<AddressDto> itDto = addressDtos.iterator();
AddressDto addressDto = itDto.next();
// Update Address with AddressDto Information
address.setAddressName(addressDto.getAddressName());
address.setAddressLine1(addressDto.getAddressLine1());
address.setAddressLine2(addressDto.getAddressLine2());
address.setAddressLine3(addressDto.getAddressLine3());
address.setCity(addressDto.getCity());
address.setState(addressDto.getState());
address.setPostalCode(addressDto.getPostalCode());
address.setCountry(addressDto.getCountry());
address.setAddressType(addressDto.getAddressType());
}
}
@Override
public Class<UpdateAccountAction> getActionType() {
return UpdateAccountAction.class;
}
@Override
public void undo(UpdateAccountAction action, UpdateAccountResult result,
ExecutionContext context) throws ActionException {
}
}
4. Delete an Account entity
We can delete an Account by selecting a record on Serendiptiy's Main page, and then clicking the
Delete button:

First, we need to register a handler for the Delete button in the nested Accounts View:
...
toolBar.addButton(ToolBar.DELETE_BUTTON,
Serendipity.getConstants().DeleteButtonTooltip(), new ClickHandler() {
public void onClick(ClickEvent event) {
if (getUiHandlers() != null) {
ListGridRecord[] records = listGrid.getSelection();
if (records.length == 1) {
String title = Serendipity.getConstants().AccountWindowTitle() +
records[0].getAttributeAsString(AccountsRecord.ACCOUNT_NAME);
accountId = records[0].getAttributeAsString(AccountsRecord.ACCOUNT_ID);
event.cancel();
SC.ask(title, "Are you sure you want to delete this Account?", new BooleanCallback()
{
@Override
public void execute(Boolean value)
{
if (value != null && value) { // Yes
getUiHandlers().onDeleteButtonClicked(accountId);
}
}
});
}
else {
listGrid.deselectAllRecords();
}
}}
}
);
We also we need to define a UI handler in the nested Accounts Presenter:
...
public void onDeleteButtonClicked(String accountId) {
Long id = -1L;
try {
id = Long.valueOf(accountId);
}
catch (NumberFormatException nfe) {
Log.debug("NumberFormatException: " + nfe.getLocalizedMessage());
return;
}
dispatcher.execute(new DeleteAccountAction(id),
new AsyncCallback<DeleteAccountResult>() {
@Override
public void onFailure(Throwable caught) {
Log.debug("onFailure() - " + caught.getLocalizedMessage());
}
@Override
public void onSuccess(DeleteAccountResult result) {
Log.debug("onSuccess()");
getView().removeSelectedData();
}
});
}
As before, we can use gwt-platform's @GenDispatch annotation to generate the 'DeleteAccount' Action and Result classes:
package au.com.uptick.serendipity.shared.action;
import com.gwtplatform.annotation.GenDispatch;
import com.gwtplatform.annotation.In;
import com.gwtplatform.dispatch.shared.UnsecuredActionImpl;
@GenDispatch(isSecure = false, serviceName = UnsecuredActionImpl.DEFAULT_SERVICE_NAME)
public class DeleteAccount {
@In(1) Long accountId;;
}
However, we'll need to provide our own DeleteAccount Handler implementation:
...
public class DeleteAccountHandler implements
ActionHandler<DeleteAccountAction, DeleteAccountResult> {
private final Provider<HttpServletRequest> requestProvider;
private final ServletContext servletContext;
@Inject
DeleteAccountHandler(final ServletContext servletContext,
final Provider<HttpServletRequest> requestProvider) {
this.servletContext = servletContext;
this.requestProvider = requestProvider;
}
@Override
public DeleteAccountResult execute(final DeleteAccountAction action,
final ExecutionContext context) throws ActionException {
DeleteAccountResult result = null;
AccountDao accountDao = new AccountDao();
DOMConfigurator.configure("log4j.xml");
Log.info("Delete Account: " + action.getAccountId());
try {
accountDao.deleteAccount(accountDao.retrieveAccount(action.getAccountId()));
result = new DeleteAccountResult();
}
catch (Exception e) {
Log.warn("Unable to delete Account - ", e);
throw new ActionException(e);
}
return result;
}
@Override
public Class<DeleteAccountAction> getActionType() {
return DeleteAccountAction.class;
}
@Override
public void undo(DeleteAccountAction action, DeleteAccountResult result,
ExecutionContext context) throws ActionException {
}
}
5. Test the application in development mode
At this point, you should be able to compile Serendipity and launch it from within Eclipse.
The German language version of Serendipity's Main page (and the nested Accounts presenter and view):

The German language version of Serendipity's Account page:

The German language version of Serendipity's Main page (and the nested Administration presenter and view):

Note: You can download the sample application from the CRMdipity (Se-r-en-dipity) project's download page.
What's Next
At this point, we've learnt how to implement the four basic functions of persistent storage. Next we'll look at how to add selection and pagination support to Serendipity's MVP components
- Tags:
- tutorial,
- SmartGWT,
- paging,
- pagination,
- ListGrid,
- JPA,
- gwt-platform,
- gwt-dispatch,
- GWT MVP,
- GWT,
- grid,
- CRUD

Comments
Gracias
Me gusto, voy a revisar tus ejemplos. Gracias por la ayuda :)
- Albert
Gilead and Spring Security
Excellent tutorial. Thank you very much.
I want to know if Gilead and Spring Security will conflict with gwt-platform.
In my opinion it doesn't. However, I want to make sure.
Regards,
gwt-platform, Spring, Gilead
Hi,
The gwt-platform team have just released a new version that includes Spring support -> http://code.google.com/p/gwt-platform/
Take a look at this post which discusses both Gilead and Dozer -> http://code.google.com/webtoolkit/articles/using_gwt_with_hibernate.html
Cheers
Rob
Excellent
Excellent Tutorial. Thank you very much.
Thank you
Excellent Tutorial Rob.
Thanks a lottt man.
BaseDao abstract methods
Hi Rob,
Thank you for your efforts. You're really doing a great job.
Actually, I have quite a stupid question. I've noticed that in the BaseDAO class there are two abstract methods: "createObject(Object object)" and "retrieveObjects(int maxResults, int firstResult)". Is it necessary to keep the two methods or can I just get rid of them?
Thanks in advance.
Re: BaseDao abstract methods
Hi,
If you highlight (select) a method in Eclispe then right-click and choose "Declarations -> Project" Eclipse will produce a list of derived classes where the method is implemented.
The createObject method is used by the FileUploadServlet in the persistEntitiy method to create objects via reflection.
The retrieveObjects method is used to retrieve up to a maximum number of objects (e.g. no more than 50) from a persistence store.
Cheers
Rob