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
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>
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
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>
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>
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.
Intro | Part I | Part II | Part III | Part IV | Part V |
Also check out The Unofficial Google Web Toolkit Blog