In This Post
We will be creating a new GWT module and linking it to our Pizza Shop application. The new module will allow Pizza Shop customers to view the status of their orders.
Targeted Functionality
Our main goal is to provide a Web interface for Pizza Shop customers to view the status of their orders. Customers will enter their phone numbers and query the status of the pizza order they've placed using the given phone number. The following six status values will be supported by the application:
- New Order
- Preparing
- Oven
- Ready
- Delivery
- Delivered
One of these six status values or an "order not found" error message will be displayed after a phone number is submitted.
The new interface will not support a view for mobile devices.
Generating OrderStatus Enumeration
Lets get started by launching the Roo Shell from within SpringSource Tool Suite and entering the following commands to create an
OrderStatus enumeration.
Welcome to Spring Roo. For assistance press TAB or type "hint" then hit ENTER.
roo> enum type --class ~.shared.OrderStatus
Created SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\shared
Created SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\shared\OrderStatus.java
~.shared.OrderStatus roo> enum constant --name NEW_ORDER
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\shared\OrderStatus.java
~.shared.OrderStatus roo> enum constant --name PREPARING
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\shared\OrderStatus.java
~.shared.OrderStatus roo> enum constant --name OVEN
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\shared\OrderStatus.java
~.shared.OrderStatus roo> enum constant --name READY
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\shared\OrderStatus.java
~.shared.OrderStatus roo> enum constant --name DELIVERY
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\shared\OrderStatus.java
~.shared.OrderStatus roo> enum constant --name DELIVERED
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\shared\OrderStatus.java
~.shared.OrderStatus roo>
Notice that Roo first creates the
OrderStatus enumeration and then updates it with each
enum constant command.
Updating PizzaOrder Entity
Those of you who have gone through the previous post will notice that we are missing a couple of things here; currently, we are storing neither phone number nor status intormation for a
PizzaOrder. We will need to add these.
Using the STS Roo Shell, we will add a new
orderStatus field to the
PizzaOrder entity with the following Roo command.
~.shared.OrderStatus roo> field enum --fieldName orderStatus --type ~.shared.OrderStatus --class ~.server.domain.PizzaOrder
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\server\domain\PizzaOrder.java
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\request\PizzaOrderProxy.java
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderMobileDetailsView_Roo_Gwt.java
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderDetailsView_Roo_Gwt.java
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderListView_Roo_Gwt.java
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderMobileEditView_Roo_Gwt.java
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderEditView_Roo_Gwt.java
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderMobileEditView.ui.xml
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderEditView.ui.xml
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderMobileDetailsView.ui.xml
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderDetailsView.ui.xml
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderMobileEditView.ui.xml
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderEditView.ui.xml
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderMobileDetailsView.ui.xml
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderDetailsView.ui.xml
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\server\domain\PizzaOrder_Roo_ToString.aj
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\server\domain\PizzaOrder_Roo_JavaBean.aj
Updated SRC_TEST_JAVA\com\blogspot\gwtsts\pizzashop\server\domain\PizzaOrderDataOnDemand_Roo_DataOnDemand.aj
~.server.domain.PizzaOrder roo>
We will also need to add a phoneNumber field to the
PizzaOrder entity.
~.server.domain.PizzaOrder roo> field string --fieldName phoneNumber --notNull --sizeMin 7
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\server\domain\PizzaOrder.java
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\request\PizzaOrderProxy.java
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderMobileDetailsView_Roo_Gwt.java
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderDetailsView_Roo_Gwt.java
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderListView_Roo_Gwt.java
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderMobileEditView_Roo_Gwt.java
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderEditView_Roo_Gwt.java
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderMobileEditView.ui.xml
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderEditView.ui.xml
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderMobileDetailsView.ui.xml
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderDetailsView.ui.xml
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderMobileEditView.ui.xml
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderEditView.ui.xml
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderMobileDetailsView.ui.xml
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\client\managed\ui\PizzaOrderDetailsView.ui.xml
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\server\domain\PizzaOrder_Roo_ToString.aj
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\server\domain\PizzaOrder_Roo_JavaBean.aj
Updated SRC_TEST_JAVA\com\blogspot\gwtsts\pizzashop\server\domain\PizzaOrderDataOnDemand_Roo_DataOnDemand.aj
~.server.domain.PizzaOrder roo>
Notice that the View files are automatically updated by Roo along with the
PizzaOrder entity and associated AspectJ files.
We are ready to move on now that our
PizzaOrder entity stores
orderStatus and customer
phoneNumber information.
Adding a Finder Using the Roo Shell
Since the requirements state that customers will be entering their phone numbers to view the status of their
PizzaOrders, we will need to query
PizzaOrders by customer phone number in the
Status module. However, this functionality is not generated with the standard Roo-generated application and we will have to use Roo's
finder add command to generate a new finder method that creates the necessary query object as shown below.
~.server.domain.PizzaOrder roo> finder add --finderName findPizzaOrdersByPhoneNumber
Updated SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\server\domain\PizzaOrder.java
Created SRC_MAIN_JAVA\com\blogspot\gwtsts\pizzashop\server\domain\PizzaOrder_Roo_Finder.aj~.server.domain.PizzaOrder roo>
This command generates the
PizzaOrder_Roo_Finder.aj AspectJ file that contains the necessary finder aspect and its
findPizzaOrdersByPhoneNumber() method as shown below.
privileged aspect PizzaOrder_Roo_Finder {
public static Query PizzaOrder.findPizzaOrdersByPhoneNumber(String phoneNumber) {
if (phoneNumber == null || phoneNumber.length() == 0) throw new IllegalArgumentException("The phoneNumber argument is required");
EntityManager em = PizzaOrder.entityManager();
Query q = em.createQuery("SELECT PizzaOrder FROM PizzaOrder AS pizzaorder WHERE pizzaorder.phoneNumber = :phoneNumber");
q.setParameter("phoneNumber", phoneNumber);
return q;
}
}
Notice that the new method returns a
Query object, which will have to be executed to retrieve
PizzaOrder objects.
Executing the Query
With Roo having generated the finder method for us, all we need to do is execute the
Query via a call to its
getResultList() method. Adding the following method to the
~.server.domain.PizzaOrder entity class will do the trick.
@RooJavaBean
@RooToString
@RooEntity(finders = { "findPizzaOrdersByPhoneNumber" })
public class PizzaOrder {
...
@SuppressWarnings("unchecked")
public static List<PizzaOrder> findPizzaOrderEntriesByPhoneNumber(String phoneNumber) {
return PizzaOrder.findPizzaOrdersByPhoneNumber(phoneNumber).getResultList();
}
}
At this point, our work on the server side has been completed, but we will need to create interfaces on the client side to be able to use our new finder method.
Request Extensions
Lets create extensions to
PizzaOrderRequest and
ApplicationRequestFactory interfaces on the client side in order to be able to use the
findPizzaOrderEntriesByPhoneNumber() method that we created on the server side.
The following files will be created in the
~.status.client.request package.
StatusPizzaOrderRequest.java:
@ServiceName("com.blogspot.gwtsts.pizzashop.server.domain.PizzaOrder")
public interface StatusPizzaOrderRequest extends PizzaOrderRequest {
abstract Request<List<PizzaOrderProxy>> findPizzaOrderEntriesByPhoneNumber(String phoneNumber);
}
StatusRequestFactory.java:
public interface StatusRequestFactory extends ApplicationRequestFactory {
StatusPizzaOrderRequest statusPizzaOrderRequest();
}
Our application is now able to query
PizzaOrders by client's
phoneNumber and we can go ahead and create our new module.
The Status Module
Our aim here is to create a new GWT module that will provide an interface for Pizza Shop users to query their
PizzaOrders. The new
Status module will inherit the Roo-generated
applicationScaffold module in order to make use of its data access functionality. Firstly, we will need to remove the
EntryPoint declaration in the
applicationScaffold module as having multiple
EntryPoints will result in successive calls to a different implementation of
onModuleLoad() for each
EntryPoint, which is not what we want. Removing the following line from
ApplicationScaffold.gwt.xml will allow us to inherit the
applicationScaffold module without the extra call to
applicationScaffold's own
onModuleLoad() method.
<entry-point class="com.blogspot.gwtsts.pizzashop.client.scaffold.Scaffold"/>
There is a side-effect to this change; with its
EntryPoint removed, the
applicationScaffold module will not run. We will have to create a new module and update our GWT host pages to allow existing functionality to work again.
Using the "New GWT Module" wizard, lets create a new module named Console in the ~.admin package. The contents of
Console.gwt.xml should be as follows.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 2.2.0//EN" "http://google-web-toolkit.googlecode.com/svn/tags/2.2.0/distro-source/core/src/gwt-module.dtd">
<module rename-to="console">
<inherits name="com.blogspot.gwtsts.pizzashop.ApplicationScaffold" />
<entry-point class="com.blogspot.gwtsts.pizzashop.client.scaffold.Scaffold"/>
</module>
Script declaration in
ApplicationScaffold.html should be updated as follows.
<!doctype html>
<html>
<head>
...
<script type="text/javascript" language="javascript"
src="console/console.nocache.js"></script>
</head>
<body style="-webkit-text-size-adjust: none;">
...
</body>
</html>
The same requirement applies to the script declaration in
console.html.
<!doctype html>
<html>
<head>
...
<script type="text/javascript" language="javascript"
src="../console/console.nocache.js"></script>
</head>
<body style="-webkit-text-size-adjust: none;">
...
</body>
</html>
With the side-effect taken care of, we can now go ahead and create the
Status module itself using the "New GWT Module" wizard. Contents of the resulting module definition file,
Status.gwt.xml, should be as follows.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 2.2.0//EN" "http://google-web-toolkit.googlecode.com/svn/tags/2.2.0/distro-source/core/src/gwt-module.dtd">
<module rename-to="status">
<inherits name="com.blogspot.gwtsts.pizzashop.ApplicationScaffold" />
<source path="client"/>
</module>
The
rename-to attribute has to be entered manually after the module definition file is generated by the wizard.
Next on our to-do list is to create a host page for our new module. We will create the following HTML page using GPE's "New HTML Page" wizard.
<!doctype html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>OrderStatus</title>
<script type="text/javascript" language="javascript" src="status/status.nocache.js"></script>
</head>
<body>
<iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0"></iframe>
</body>
</html>
At this point, our module and HTML page have been created and we are ready to start creating our module's
EntryPoint,
View and
Presenter classes.
GIN Support For the Status Module
We could have chosen not to use GIN since we will not be creating a mobile view, but we will use it in order to stay in line with the Roo-generated
applicationScaffold module and to be ready in case we decide to add a mobile view in the future.
The following classes and interfaces will be created in the
~.status.client.ioc package.
StatusModule.java:
public class StatusModule extends AbstractGinModule {
@Override
protected void configure() {
bind(EventBus.class).to(SimpleEventBus.class).in(Singleton.class);
bind(StatusRequestFactory.class).toProvider(RequestFactoryProvider.class).in(Singleton.class);
bind(PlaceController.class).toProvider(PlaceControllerProvider.class).in(Singleton.class);
}
static class PlaceControllerProvider implements Provider<PlaceController> {
private final PlaceController placeController;
@Inject
public PlaceControllerProvider(EventBus eventBus) {
this.placeController = new PlaceController(eventBus);
}
public PlaceController get() {
return placeController;
}
}
static class RequestFactoryProvider implements Provider<StatusRequestFactory> {
private final StatusRequestFactory requestFactory;
@Inject
public RequestFactoryProvider(EventBus eventBus) {
requestFactory = GWT.create(StatusRequestFactory.class);
requestFactory.initialize(eventBus, new EventSourceRequestTransport(
eventBus, new GaeAuthRequestTransport(eventBus)));
}
public StatusRequestFactory get() {
return requestFactory;
}
}
}
StatusInjector.java:
public interface StatusInjector extends Ginjector {
StatusApp getStatusApp();
}
StatusDesktopInjector.java:
@GinModules(value = {StatusModule.class})
public interface StatusDesktopInjector extends StatusInjector {
@Override
public StatusDesktopApp getStatusApp();
}
StatusInjectorWrapper.java:
public interface StatusInjectorWrapper {
StatusInjector getInjector();
}
StatusDesktopInjectorWrapper.java:
public class StatusDesktopInjectorWrapper implements StatusInjectorWrapper {
@Override
public StatusInjector getInjector() {
return GWT.create(StatusDesktopInjector.class);
}
}
Status Module's EntryPoint
Using the "New Entry Point Class" wizard, lets create the following GWT
EntryPoint class for the
Status module. The wizard will automatically add the
entry-point configuration to our module definition file.
Status.java:
public class Status implements EntryPoint {
final private StatusInjectorWrapper injectorWrapper = GWT.create(StatusDesktopInjectorWrapper.class);
@Override
public void onModuleLoad() {
injectorWrapper.getInjector().getStatusApp().run();
}
}
Following the pattern set by the
Scaffold module, the following classes will be created in the
~.status.client package.
StatusApp.java:
public class StatusApp {
static boolean isMobile = false;
public static boolean isMobile() {
return isMobile;
}
public void run() {
}
}
StatusDesktopApp.java:
public class StatusDesktopApp extends StatusApp {
private final StatusDesktopShell shell;
private final StatusRequestFactory requestFactory;
private final EventBus eventBus;
private final PlaceController placeController;
private final StatusPlaceHistoryFactory placeHistoryFactory;
private final StatusActivityMapper statusActivityMapper;
@Inject
public StatusDesktopApp(StatusRequestFactory requestFactory, EventBus eventBus,
PlaceController placeController, StatusPlaceHistoryFactory placeHistoryFactory,
StatusActivityMapper statusActivityMapper) {
this.shell = StatusDesktopShell.instance();
this.requestFactory = requestFactory;
this.eventBus = eventBus;
this.placeController = placeController;
this.placeHistoryFactory = placeHistoryFactory;
this.statusActivityMapper = statusActivityMapper;
}
public void run() {
/* Add handlers, setup activities */
init();
/* And show the user the shell */
RootLayoutPanel.get().add(shell);
}
private void init() {
// AppEngine user authentication
new ReloadOnAuthenticationFailure().register(eventBus);
CachingActivityMapper activityMapper = new CachingActivityMapper(statusActivityMapper);
final ActivityManager activityManager = new ActivityManager(activityMapper, eventBus);
activityManager.setDisplay(shell.getDisplay());
StatusPlaceHistoryMapper mapper = GWT.create(StatusPlaceHistoryMapper.class);
mapper.setFactory(placeHistoryFactory);
PlaceHistoryHandler placeHistoryHandler = new PlaceHistoryHandler(mapper);
Place defaultPlace = new StatusQueryPlace();
placeHistoryHandler.register(placeController, eventBus, defaultPlace);
placeHistoryHandler.handleCurrentHistory();
}
}
The
@Inject annotation marks the constructor to be used by GIN. GIN instantiates all arguments to the "annotated" constructor in the same manner and uses the default constructor in case a constructor with an
@Inject annotation can't be found.
The call to
placeHistoryHandler.handleCurrentHistory() method in
StatusDesktopApp.init() is where the first
PlaceChangeEvent is triggered. This
Place change to the default
StatusQueryPlace allows the first
Activity, the
StatusQueryActivity, to be
start()ed.
The View
The following classes and UiBinder files will be created in the
~.status.client.ui package using the "New UiBinder" wizard.
StatusDesktopShell.java:
public class StatusDesktopShell extends Composite {
private static StatusDesktopShellUiBinder uiBinder = GWT
.create(StatusDesktopShellUiBinder.class);
@UiField
SimplePanel display;
interface StatusDesktopShellUiBinder extends
UiBinder<Widget, StatusDesktopShell> {
}
public StatusDesktopShell() {
initWidget(uiBinder.createAndBindUi(this));
}
public SimplePanel getDisplay() {
return display;
}
}
StatusDesktopShell.ui.xml:
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
xmlns:g="urn:import:com.google.gwt.user.client.ui">
<ui:style>
@def contentWidth 850px;
.banner {
background-color: #777;
-moz-border-radius-topleft: 10px;
-webkit-border-top-left-radius: 10px;
-moz-border-radius-topright: 10px;
-webkit-border-top-right-radius: 10px;
margin-top: 1.5em;
height: 4em;
}
.title {
color: white;
padding: 1em;
position: absolute;
color: #def;
}
.title h2 {
margin: 0;
}
.centered {
width: contentWidth;
margin-right: auto;
margin-left: auto;
}
</ui:style>
<g:DockLayoutPanel unit='EM'>
<g:north size='6'>
<g:HTMLPanel styleName='{style.centered}'>
<div class='{style.banner}'>
<span class='{style.title}'>
<h2>Pizza Order Status</h2>
</span>
</div>
</g:HTMLPanel>
</g:north>
<g:south size='2'>
<g:HTML/>
</g:south>
<g:center>
<g:SimplePanel styleName='{style.centered}' ui:field='display'/>
</g:center>
</g:DockLayoutPanel>
</ui:UiBinder>
StatusDesktopShell forms the frame or shell for other views in our module. The
SimplePanel labeled 'display' is the display region that is passed to our
ActivityManager in
StatusDesktopApp.init(). The
ActivityManager will pass this display region on to each
Activity it manages through the first argument of the
Activity's
start() method.
StatusQueryView.java:
public class StatusQueryView extends Composite implements View<StatusQueryView> {
private static StatusQueryViewUiBinder uiBinder = GWT
.create(StatusQueryViewUiBinder.class);
private static StatusQueryView instance;
interface StatusQueryViewUiBinder extends UiBinder<Widget, StatusQueryView> {
}
@UiField
TextBox phoneNumber;
@UiField
Button query;
private StatusQueryProxyView.Delegate delegate;
public StatusQueryView() {
initWidget(uiBinder.createAndBindUi(this));
}
public static StatusQueryView instance() {
if (instance == null)
instance = new StatusQueryView();
return instance;
}
@UiHandler("query")
void onQuery(ClickEvent event) {
delegate.queryClicked();
}
@Override
public void setDelegate(StatusQueryProxyView.Delegate delegate) {
this.delegate = delegate;
}
@Override
public String getPhoneNumber() {
return phoneNumber.getText();
}
}
StatusQueryView.ui.xml:
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
xmlns:g="urn:import:com.google.gwt.user.client.ui">
<ui:style>
</ui:style>
<g:HTMLPanel>
<table>
<tr>
<td>
<div>Phone Number:</div>
</td>
<td>
<g:TextBox ui:field='phoneNumber' />
</td>
<td>
<g:Button ui:field='query'>Query</g:Button>
</td>
</tr>
</table>
</g:HTMLPanel>
</ui:UiBinder>
StatusQueryView is shown in the display region when
StatusQueryActivity is
start()ed.
StatusDisplayView.java:
public class StatusDisplayView extends Composite implements
View<StatusDisplayView> {
private static StatusDisplayViewUiBinder uiBinder = GWT
.create(StatusDisplayViewUiBinder.class);
private static StatusDisplayView instance;
interface StatusDisplayViewUiBinder extends
UiBinder<Widget, StatusDisplayView> {
}
@UiField
DivElement errors;
@UiField
Label status;
public StatusDisplayView() {
initWidget(uiBinder.createAndBindUi(this));
}
public static StatusDisplayView instance() {
if (instance == null)
instance = new StatusDisplayView();
return instance;
}
@Override
public void showErrors(List<EditorError> errors) {
SafeHtmlBuilder b = new SafeHtmlBuilder();
for (EditorError error : errors) {
b.appendEscaped(error.getPath()).appendEscaped(": ");
b.appendEscaped(error.getMessage()).appendHtmlConstant("<br>");
}
this.errors.setInnerHTML(b.toSafeHtml().asString());
}
@Override
public void setStatus(String status) {
this.status.setText(status);
}
}
StatusDisplayView.ui.xml:
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
xmlns:g="urn:import:com.google.gwt.user.client.ui">
<ui:style>
.errors {
padding-left: 0.5em;
background-color: red;
}
</ui:style>
<g:HTMLPanel>
<div ui:field='errors' class='{style.errors}'></div>
<table>
<tr>
<td>
<div>Status:</div>
</td>
<td>
<g:Label ui:field='status' />
</td>
</tr>
</table>
</g:HTMLPanel>
</ui:UiBinder>
The display region switches to the
StatusDisplayView when
StatusDisplayActivity is
start()ed as a result of a
Place change to
StatusDisplayPlace.
Status Module's Places
There will be two
Places in the
Status module; a
Place where customers will query the status of their pizza order and another where the status of their
PizzaOrder will be displayed. The following
Places will be created in the
~.status.client.place package.
StatusQueryPlace.java:
public class StatusQueryPlace extends Place {
/**
* Tokenizer.
*/
public static class Tokenizer implements PlaceTokenizer<StatusQueryPlace> {
public StatusQueryPlace getPlace(String token) {
return new StatusQueryPlace();
}
public String getToken(StatusQueryPlace place) {
return place.getClass().getName();
}
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
return true;
}
}
The
StatusQueryPlace will be a simple
Place with no additional fields.
StatusDisplayPlace.java:
public class StatusDisplayPlace extends Place {
private static final String SEPARATOR = "!";
/**
* Tokenizer.
*/
public static class Tokenizer implements PlaceTokenizer<StatusDisplayPlace> {
public StatusDisplayPlace getPlace(String token) {
String bits[] = token.split(SEPARATOR);
return new StatusDisplayPlace(bits[1]);
}
public String getToken(StatusDisplayPlace place) {
return place.getClass().getName() + SEPARATOR
+ place.getPhoneNumber();
}
}
private String phoneNumber;
public StatusDisplayPlace(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
return true;
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
}
The
StatusDisplayPlace will be used to signal that a
phoneNumber has been queried and pass this
phoneNumber on to the
StatusDisplayActivity.
History Management
The following files will be created in the
~.status.client.place package.
StatusPlaceHistoryFactory.java:
public class StatusPlaceHistoryFactory {
private final StatusQueryPlace.Tokenizer queryPlaceTokenizer;
private final StatusDisplayPlace.Tokenizer displayPlaceTokenizer;
@Inject
public StatusPlaceHistoryFactory() {
this.queryPlaceTokenizer = new StatusQueryPlace.Tokenizer();
this.displayPlaceTokenizer = new StatusDisplayPlace.Tokenizer();
}
public PlaceTokenizer<StatusQueryPlace> getQueryPlaceTokenizer() {
return queryPlaceTokenizer;
}
public PlaceTokenizer<StatusDisplayPlace> getDisplayPlaceTokenizer() {
return displayPlaceTokenizer;
}
}
StatusPlaceHistoryMapper.java:
public interface StatusPlaceHistoryMapper extends PlaceHistoryMapperWithFactory<StatusPlaceHistoryFactory> {
}
StatusPlaceHistoryFactory provides the accessor methods to
PlaceTokenizers for all
Places used in the module. These
PlaceTokenizers are passed to
StatusPlaceHistoryMapper, which, together with the
PlaceHistoryHandler, allows the forward and back buttons of your Web browser work.
Proxy Views
The following files will be created in the
~.status.client.place package.
StatusProxyView.java:
public interface StatusProxyView<P extends EntityProxy, V extends StatusProxyView<P, V>>
extends IsWidget {
}
StatusDisplayProxyView.java:
public interface StatusDisplayProxyView<P extends EntityProxy, V extends StatusProxyView<P, V>>
extends StatusProxyView<P, V>, HasEditorErrors<P> {
public void setStatus(String status);
}
StatusQueryProxyView.java:
public interface StatusQueryProxyView<P extends EntityProxy, V extends StatusProxyView<P, V>>
extends StatusProxyView<P, V> {
/**
* Implemented by the owner of the view.
*/
interface Delegate {
void queryClicked();
}
void setDelegate(Delegate delegate);
String getPhoneNumber();
}
These
ProxyViews are
Presenter-side representations for our
View implementations and provide a layer of abstraction between the
Presenter and the
View components of our MVP model.
Implementing the Presenter
Activity classes make up the
Presenter component of the MVP model. The following
Activity classes will be created in the
~.status.client.activity package.
StatusQueryActivity.java:
public class StatusQueryActivity extends AbstractActivity implements StatusQueryProxyView.Delegate {
private PlaceController placeController;
private StatusQueryProxyView<PizzaOrderProxy, ?> view;
public StatusQueryActivity(PlaceController placeController,
StatusQueryProxyView<PizzaOrderProxy, ?> view, StatusRequestFactory requests) {
this.placeController = placeController;
this.view = view;
}
@Override
public void start(AcceptsOneWidget panel, EventBus eventBus) {
view.setDelegate(this);
panel.setWidget(view);
}
@Override
public void queryClicked() {
placeController.goTo(new StatusDisplayPlace(view.getPhoneNumber()));
}
}
StatusQueryActivity is the default
Activity that is
start()ed when the
Status module is loaded. It responds to clicks on the Query button by extracting the
phoneNumber from the
View and changing the current
Place to
StatusDisplayPlace.
StatusDisplayActivity.java:
public class StatusDisplayActivity extends AbstractActivity {
private PlaceController placeController;
private StatusDisplayProxyView<PizzaOrderProxy, ?> view;
private StatusRequestFactory requests;
public StatusDisplayActivity(PlaceController placeController,
StatusDisplayProxyView<PizzaOrderProxy, ?> view, StatusRequestFactory requests) {
this.placeController = placeController;
this.view = view;
this.requests = requests;
}
@Override
public void start(AcceptsOneWidget panel, EventBus eventBus) {
Place place = placeController.getWhere();
if (place instanceof StatusDisplayPlace) {
StatusDisplayPlace displayPlace = (StatusDisplayPlace) place;
requests.statusPizzaOrderRequest().findPizzaOrderEntriesByPhoneNumber(displayPlace.getPhoneNumber()).fire(new Receiver<List<PizzaOrderProxy>>() {
@Override
public void onSuccess(List<PizzaOrderProxy> response) {
if (response.size() > 0) {
view.setStatus(response.get(0).getOrderStatus().toString());
}
else {
view.setStatus("Order not found");
}
}
@Override
public void onFailure(ServerFailure error) {
view.setStatus(error.getMessage());
}
});
}
panel.setWidget(view);
}
}
StatusQueryActivity is
start()ed when the current
Place is changed to
StatusDisplayPlace. It executes the finder method that retrieves all
PizzaOrders with maching
phoneNumber values from the server. If the request is successfully processed, then the
onSuccess() method is called with the server response as its argument. The implementation shown above displays only the first item on the list or an error message if the list is empty.
StatusActivityMapper.java:
public class StatusActivityMapper implements ActivityMapper {
private PlaceController placeController;
private StatusRequestFactory requests;
@Inject
public StatusActivityMapper(StatusRequestFactory requests, PlaceController placeController) {
this.requests = requests;
this.placeController = placeController;
}
@Override
public Activity getActivity(Place place) {
if (place instanceof StatusQueryPlace)
return makeQueryActivity();
if (place instanceof StatusDisplayPlace)
return makeDisplayActivity();
return null;
}
private Activity makeQueryActivity() {
Activity activity = new StatusQueryActivity(placeController,
StatusQueryView.instance(), requests);
return new StatusQueryActivityWrapper(activity, requests, StatusQueryView.instance());
}
private Activity makeDisplayActivity() {
Activity activity = new StatusDisplayActivity(placeController,
StatusDisplayView.instance(), requests);
return new StatusDisplayActivityWrapper(activity, requests, StatusDisplayView.instance());
}
}
The
StatusActivityMapper's sole job is to map
Places to corresponding
Activitys. The
ActivityManager calls its
getActivity() method in response to
PlaceChangeEvents to get the
Activity that corresponds to the next
Place.
StatusActivityWrapper.java:
abstract public class StatusActivityWrapper implements Activity {
protected Activity wrapped;
protected ApplicationRequestFactory requests;
@Override
public String mayStop() {
return wrapped.mayStop();
}
@Override
public void onCancel() {
wrapped.onCancel();
}
@Override
public void onStop() {
wrapped.onStop();
}
@Override
public void start(AcceptsOneWidget panel, EventBus eventBus) {
wrapped.start(panel, eventBus);
}
}
StatusQueryActivityWrapper.java:
public class StatusQueryActivityWrapper extends StatusActivityWrapper {
public interface View<V extends StatusQueryProxyView<PizzaOrderProxy, V>> extends StatusQueryProxyView<PizzaOrderProxy, V> {
}
protected View<?> view;
public StatusQueryActivityWrapper(Activity activity, ApplicationRequestFactory requests, View<?> view) {
this.wrapped = activity;
this.requests = requests;
this.view = view;
}
}
StatusDisplayActivityWrapper.java:
public class StatusDisplayActivityWrapper extends StatusActivityWrapper {
public interface View<V extends StatusDisplayProxyView<PizzaOrderProxy, V>> extends StatusDisplayProxyView<PizzaOrderProxy, V> {
}
protected View<?> view;
public StatusDisplayActivityWrapper(Activity activity, ApplicationRequestFactory requests, View<?> view) {
this.wrapped = activity;
this.requests = requests;
this.view = view;
}
}
The
ActivityWrappers provide a layer of abstraction between the wrapped
Activity and the
Presenter-side representation of the
View.
Source Code
I have uploaded the source code for the Pizza Shop Web application including all changes made in this post. It can be downloaded via this
link.
Uploaded to App Engine
The new version of Pizza Shop application has been uploaded to App Engine and can be viewed at
http://pizzashopexample.appspot.com/OrderStatus.html. The order submitted with phone number '1234567' should have a status of 'NEW_ORDER' and '7654321' should be in the 'OVEN'.
What is Next?
Check out my
review of Ashish Sarin's
Spring Roo 1.1 Cookbook. I recommend this book to everyone who wants to learn more about Roo or to add Spring Security to their application.