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:

  1. Create an Account entity
  2. Retrieve an Account entity
  3. Update an Account entity
  4. Delete an Account entity
  5. 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

Selection and pagination with GWT, GWTP, smartGWT and JPA

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