If you have found your way to this blog, it is highly probable that you know what Spring Roo is, but in order to keep with the tradition of technical book reviews, I will provide a quick description.
SPRING ROO
Roo is a rapid application development (RAD) tool for Java developers that gets you started in building Java applications quickly. In its simplest form, it is a command-line tool with its own shell, supporting commands that allow you to define the characteristics of the application you wish to create. You define your data objects and select the technologies that you wish to work with from a list that includes Spring, GWT, Hibernate, Spring Security, and Flex. As you enter these commands, your application is generated for you and split into customizable code and code that is maintained by Roo. As you make changes to your application, Roo automatically makes all necessary updates to the code it maintains. Did I mention that it has an Eclipse plug-in? ... and that Roo can be removed at will from your generated application?
THE BOOK
"Spring Roo 1.1 Cookbook" by Ashish Sarin is an attempt to reveal all of Roo's secrets and present them to the readers in cookbook fashion.
I'll get started by giving some information about the book's structure and organization. It contains over four hundred pages with twenty-eight of them devoted to the GWT add-on. The book is divided into 7 chapters, each of which contain several recipes. Every recipe has sections titled "Getting ready", "How to do it", "How it works", and "There is more". The "How to do it" section takes you through each step of action necessary to accomplish goals defined in the recipe's introduction. "How it works" sections are especially useful as they explain what happens behind the scenes during every step, side effects of each action, and the recipe's impact on the big picture.
The "There is more" sections are a delight to read as they analyze how Roo responds in every possible circumstance, provide useful information on the trade-offs that accompany decisions made at design time, discuss short-comings of Roo, and describe how to workaround them - all in a clear and easy-to-understand manner. In each chapter and almost each recipe, the book managed to provide information that I hadn't known about.
The Roo scripts, "source code," that accompany the book as a separate download
are helpful in getting started with especially the more advanced
recipes as they build on tasks accomplished in previous recipes.
Absolutely thorough on its main subject, this book does
not leave anything unwritten about Roo. It is so impressively detailed
and informative that it would take months to compile all the information
contained in the book through Web research. It
provides information about how to use Roo as well as Roo's inner
workings. Every possible Roo recipe is covered; from setting up
validation to generating finder methods that scan multiple fields, to
reverse engineering a database and configuring spring security with your
web application.
"Spring Roo 1.1 Cookbook" covers Roo versions up to and including version 1.1.5, which is the latest official release.
The author doesn't just provide information on Roo; he expands the scope by giving information on all the technologies that Roo supports, including information on how to customize a Roo-generated Spring MVC application. By reading this book, I have gained valuable knowledge on technologies that I haven't had a chance to work with.
I know that I have only made positive comments so far, but only because the book deserves it. If I were to be forced to find one negative aspect of the book, then I would hesitatingly point my finger at lack of recipes on customizing Roo-generated GWT and Flex applications, which would have been nice extras to have.
CONCLUSION
In my opinion, the author, Ashish Sarin, is highly successful in his attempt to unleash all of Roo's power and uncover its secrets, excelling in the areas of clarity and attention to detail. The book contains a myriad of information, which includes everything there is to know about Spring Roo 1.1. It certainly looks and feels like the end result of considerable effort and hard work. If you liked my posts on this blog, then you'll most definitely love this book and its simple yet detailed cookbook style.
THE FIRST CHAPTER
You can download the first chapter of "Spring Roo 1.1 Cookbook" by Ashish Sarin from this link for free.
GWT with Roo and STS
Rapid Web application development using Google Web Toolkit (GWT) with Spring Roo and SpringSource Tool Suite (STS)
Saturday, November 26, 2011
Tuesday, November 8, 2011
Spring Roo 1.1 Cookbook, by Ashish Sarin
Dear Spring Roo fans,
I have recently come across a book that covers everything that I have gone through in this blog, but in greater detail and adds a lot more on top. The book is titled "Spring Roo 1.1 Cookbook" and is written by the Java guru Ashish Sarin. Here is the link:
I have briefly skimmed over the contents and it looks very promising indeed. In the coming weeks, I will be posting a review of the book on this blog.
So, I'll be reporting back soon!
Cheers,
Cengiz
I have recently come across a book that covers everything that I have gone through in this blog, but in greater detail and adds a lot more on top. The book is titled "Spring Roo 1.1 Cookbook" and is written by the Java guru Ashish Sarin. Here is the link:
I have briefly skimmed over the contents and it looks very promising indeed. In the coming weeks, I will be posting a review of the book on this blog.
So, I'll be reporting back soon!
Cheers,
Cengiz
Thursday, April 21, 2011
Part V - Creating a New GWT Module
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:
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.
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.
We will also need to add a phoneNumber field to the PizzaOrder entity.
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.
This command generates the PizzaOrder_Roo_Finder.aj AspectJ file that contains the necessary finder aspect and its findPizzaOrdersByPhoneNumber() method as shown below.
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.
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:
StatusRequestFactory.java:
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.
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.
Script declaration in ApplicationScaffold.html should be updated as follows.
The same requirement applies to the script declaration in console.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.
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.
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:
StatusInjector.java:
StatusDesktopInjector.java:
StatusInjectorWrapper.java:
StatusDesktopInjectorWrapper.java:
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:
Following the pattern set by the Scaffold module, the following classes will be created in the ~.status.client package.
StatusApp.java:
StatusDesktopApp.java:
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:
StatusDesktopShell.ui.xml:
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:
StatusQueryView.ui.xml:
StatusQueryView is shown in the display region when StatusQueryActivity is start()ed.
StatusDisplayView.java:
StatusDisplayView.ui.xml:
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:
The StatusQueryPlace will be a simple Place with no additional fields.
StatusDisplayPlace.java:
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:
StatusPlaceHistoryMapper.java:
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:
StatusDisplayProxyView.java:
StatusQueryProxyView.java:
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:
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:
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:
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:
StatusQueryActivityWrapper.java:
StatusDisplayActivityWrapper.java:
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.
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
Labels:
App Engine,
GAE,
Google,
GWT,
Rapid Application Development,
Roo,
SpringSource,
STS,
Web Application Development
Wednesday, March 30, 2011
Part IV - Customization of App Engine Authentication for Roo-Generated GWT Projects
In This Post
We are going to make the Roo-generated ApplicationScaffold.html host page accessible only by admin users and create a second host page visible to all users. While regular users will only see the new host page, admin users will be able to switch back and forth. We are going to implement this functionality without creating a new GWT module and leave this topic to be discussed in my next post.
Before We Start
I have updated the previous post with a fix that needs to be applied to the Roo-generated code before proceeding to the next section. For more information on this fix, please follow this link.
Before jumping into the customization of our Pizza Shop project, lets go through some prerequisite facts that we need to know.
Google App Engine (GAE) Login Functionality
The first screen we are presented with when we launch our Pizza Shop Web application consists of a mock GAE login form. This mock login form allows us to test our application's integration with the GAE Users API and its login functionality without having to deploy our application. There is no actual authentication taking place in this mock login screen. Anything entered will be accepted. The "Sign in as Administrator" checkbox allows us to test how our application handles users with administrative privileges.
GWT Wizards
The Wizards that GWT has made available for us will come in handy in this post as well as the next one. In order to access these wizards, select the "New" submenu's "Other..." item from the project's context menu.
Scroll down to the "Google Web Toolkit" folder to view all GWT wizards available.
We will be working with "HTML Page" and "UiBinder" wizards in this post.
Servlet Filter for Authentication
Our Roo-generated scaffold application uses the ~.server.gae.GaeAuthFilter servlet filter to grant access only to those users who have logged in to the application.
GaeAuthFilter rejects requests made by users who are not logged in by sending an error to the client, which in turn redirects the users to the login screen.
Filter declaration and mapping of GaeAuthFilter is done in the deployment descriptor of our project.
The filter-mapping element contains the "/gwtRequest/*" URL pattern, which covers all GWT requests at the root directory level.
You may be asking yourself why we need the security constraint configuration if authorization is handled by the GaeAuthFilter.
The security constraint configuration will provide security for all non-GWT HTML files in our Web application.
URL Pattern Syntax
The following is valid syntax for URL patterns defined in the deployment descriptor.
URL Pattern Processing
The container will try find a match in the following order and stop searching when a match is found.
Targeted Functionality
We are now ready to define how our Web application will behave after we have implemented our customizations. We are targeting the following logical flow in our Web application:
Implementation
We will start our customization by creating a new GWT host page for our administration console.
After adding a viewport meta tag, editing the title, and adding a span tag with a message stating that the console is being loaded, we will end up with the following.
GaeAdminFilter behaves exactly the same way as GaeAuthFilter, except that it requires users to have admin privileges in addition to being logged in. It also sends a different error code in the case that these requirements are not met.
Notice that the URL pattern for GaeAdminFilter is longer than that of GaeAuthFilter and thus will have higher priority as per URL pattern processing rules described above.
Without this servlet mapping configuration, we would receive "HTTP 404 - Not Found" error when attempting to make a GWT request from our console.html file. Note that all mapping elements, including this servlet mapping element, have to be placed after the corresponding declaration, which is the requestFactory servlet declaration in this case.
At this point, our new HTML host page is only accessible by admin users but displays the same content as the original host page. You should be able to run your application and verify that admin users can view both host pages, whereas normal users will be redirected to the login screen when attempting to access console.html.
The next task on our to-do list is to change the content that is displayed by each host page. We want the new console.html to display the original scaffold application, which we intend to use as an administration console, and the ApplicationScaffold.html to display new content.
We will need to add LoginWidget field and its accessor method to CustomView.java to give us the following.
Notice that the LoginWidget initialization by GaeLoginWidgetDriver has moved from the init() method to the run() method as the instance of LoginWidget to be initialized depends on the host page being accessed.
At this point, our new HTML host pages are displaying different content. The original scaffold view has become our administration console, accessible only by admin users through admin/console.html, and our new view is accessible by all registered users through ApplicationScaffold.html.
Now, lets link the two pages to each other by adding new Anchor widgets to both host pages. We want a link to the administration console that is only visible to users with administrative privileges on CustomView and a link that will return admin users back to the CustomView on the administration console (ScaffoldDesktopShell).
Adding an Anchor field and a setReturnUrl() method to ReturnLinkWidget.java will give us the following.
Additionally, we will need to add the ReturnLinkWidget field and accessor method to ScaffoldDesktopShell.java.
You should now be able to see this new link by accessing admin/console.html as an admin user. Clicking the link should take you back to the CustomView.
We need to create one last widget that links admin users to the administration console before we can start selling pizzas online; however, this task is not going to be as easy as ReturnLinkWidget, because we have the requirement of making the link visible only for admin users. In order to accomplish this, we need to know if users have admin privileges on the client side, which means that we will be creating a new service request method.
We are now ready to create the ConsoleLinkWidget and add it to CustomView.
Notice that we have added a call to Widget's setVisible() method in the constructor. This is because we want the Widget to be invisible by default.
Additionally, we will need to add the ConsoleLinkWidget field and accessor method to CustomView.java.
We have achieved all our objectives at this point. Only admin users are able to see a link that takes them to the admin console at this point. They are able to follow this link to the admin console and follow the return link back to the main view.
Source Code
I have zipped up and uploaded the source directory for the Pizza Shop project. The zip file includes all changes made in this post and can be downloaded via this link.
Uploaded To App Engine
I have also deployed the current state of the project to App Engine and it can be accessed via http://1.pizzashopexample.appspot.com. IE9 users might have problems launching the application.
Deploying to App Engine is very easy. Sign up to App Engine, register an application, and enter application ID in appengine-web.xml prior to deploying. Then select "Google > Deploy to App Engine" from project's context menu, enter Google username and password, and you are done!
In The Next Post
We will be creating a new GWT module and linking two modules to each other.
We are going to make the Roo-generated ApplicationScaffold.html host page accessible only by admin users and create a second host page visible to all users. While regular users will only see the new host page, admin users will be able to switch back and forth. We are going to implement this functionality without creating a new GWT module and leave this topic to be discussed in my next post.
Before We Start
I have updated the previous post with a fix that needs to be applied to the Roo-generated code before proceeding to the next section. For more information on this fix, please follow this link.
Before jumping into the customization of our Pizza Shop project, lets go through some prerequisite facts that we need to know.
Google App Engine (GAE) Login Functionality
The first screen we are presented with when we launch our Pizza Shop Web application consists of a mock GAE login form. This mock login form allows us to test our application's integration with the GAE Users API and its login functionality without having to deploy our application. There is no actual authentication taking place in this mock login screen. Anything entered will be accepted. The "Sign in as Administrator" checkbox allows us to test how our application handles users with administrative privileges.
GWT Wizards
The Wizards that GWT has made available for us will come in handy in this post as well as the next one. In order to access these wizards, select the "New" submenu's "Other..." item from the project's context menu.
Scroll down to the "Google Web Toolkit" folder to view all GWT wizards available.
We will be working with "HTML Page" and "UiBinder" wizards in this post.
Servlet Filter for Authentication
Our Roo-generated scaffold application uses the ~.server.gae.GaeAuthFilter servlet filter to grant access only to those users who have logged in to the application.
public class GaeAuthFilter implements Filter { public void destroy() { } public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { UserService userService = UserServiceFactory.getUserService(); HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; if (!userService.isUserLoggedIn()) { String requestUrl = request.getHeader("requestUrl"); if (requestUrl == null) { requestUrl = request.getRequestURI(); } response.setHeader("login", userService.createLoginURL(requestUrl)); response.sendError(HttpServletResponse.SC_UNAUTHORIZED); return; } filterChain.doFilter(request, response); } public void init(FilterConfig config) { } }
GaeAuthFilter rejects requests made by users who are not logged in by sending an error to the client, which in turn redirects the users to the login screen.
Filter declaration and mapping of GaeAuthFilter is done in the deployment descriptor of our project.
<filter> <filter-name>GaeAuthFilter</filter-name> <filter-class>com.blogspot.gwtsts.pizzashop.server.gae.GaeAuthFilter</filter-class> </filter>
<filter-mapping> <filter-name>GaeAuthFilter</filter-name> <url-pattern>/gwtRequest/*</url-pattern> </filter-mapping>
The filter-mapping element contains the "/gwtRequest/*" URL pattern, which covers all GWT requests at the root directory level.
You may be asking yourself why we need the security constraint configuration if authorization is handled by the GaeAuthFilter.
<security-constraint> <display-name>Redirect to the login page if needed before showing any html pages</display-name> <web-resource-collection> <web-resource-name>Login required</web-resource-name> <url-pattern>*.html</url-pattern> </web-resource-collection> <auth-constraint> <role-name>*</role-name> </auth-constraint> </security-constraint>
The security constraint configuration will provide security for all non-GWT HTML files in our Web application.
URL Pattern Syntax
The following is valid syntax for URL patterns defined in the deployment descriptor.
- Strings beginning with a / character and ending with a /* suffix are used for path mapping
- Strings beginning with a *. prefix are used in extension mapping
- The / character alone indicates a default mapping for the application
URL Pattern Processing
The container will try find a match in the following order and stop searching when a match is found.
- Exact match of the request path to a configured path
- Longest configured path matching the request path
- If the request path ends with an extension (ie. .html), then the container will search for a matching extension mapping
- If a default mapping exists, then the container will use it
Targeted Functionality
We are now ready to define how our Web application will behave after we have implemented our customizations. We are targeting the following logical flow in our Web application:
- A user logs in to the Web application
- The user is presented with a new custom screen
- If the user has GAE administrative privileges, then a link to the original scaffold screen is displayed. This link will not be visible for regular users without admin privileges.
- Admin users who follow this link will be taken to a new host page that displays the original scaffold screen.
- The scaffold screen will contain a link to the entry screen. Admin users will return to the new screen when this link is clicked.
Implementation
We will start our customization by creating a new GWT host page for our administration console.
- Create admin/console.html host page using "HTML Page" GWT wizard.
After adding a viewport meta tag, editing the title, and adding a span tag with a message stating that the console is being loaded, we will end up with the following.
<!doctype html> <html> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width"> <title>Administration Console</title> <script type="text/javascript" language="javascript" src="../applicationScaffold/applicationScaffold.nocache.js"></script> </head> <body> <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0"></iframe> <span id='loading' style="margin-left:10px;font-size:12px;padding:10px;font-family:Helvetica;background-color:#e5edf9;border:2px solid #96a2b5;"> Loading Admin Console… </span> </body> </html>
- ApplicationScaffold.html will be the host page accessible for all users, so lets edit the title in ApplicationScaffold.html to something more appropriate.
<title>Pizza Shop</title>
- Now that we have two different host pages, the time has come to limit access to one of them. We will start by creating a new filter class in the ~.server.gae package.
public class GaeAdminFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { UserService userService = UserServiceFactory.getUserService(); HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; if (!userService.isUserLoggedIn() || !userService.isUserAdmin()) { String requestUrl = request.getHeader("requestUrl"); if (requestUrl == null) { requestUrl = request.getRequestURI(); } response.setHeader("login", userService.createLoginURL(requestUrl)); response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } chain.doFilter(request, response); } @Override public void destroy() { } }
GaeAdminFilter behaves exactly the same way as GaeAuthFilter, except that it requires users to have admin privileges in addition to being logged in. It also sends a different error code in the case that these requirements are not met.
- We have created GaeAdminFilter, but it won't be used unless we add it to our deployment descriptor. So, lets insert the filter declaration and filter mapping for GaeAdminFilter into our web.xml file.
<filter> <filter-name>GaeAdminFilter</filter-name> <filter-class>com.blogspot.gwtsts.pizzashop.server.gae.GaeAdminFilter</filter-class> </filter>
<filter-mapping> <filter-name>GaeAdminFilter</filter-name> <url-pattern>/admin/gwtRequest/*</url-pattern> </filter-mapping>
Notice that the URL pattern for GaeAdminFilter is longer than that of GaeAuthFilter and thus will have higher priority as per URL pattern processing rules described above.
- Another change that we have to make to the deployment descriptor is to add a requestFactory servlet mapping for the new admin directory as shown below.
<servlet-mapping> <servlet-name>requestFactory</servlet-name> <url-pattern>/admin/gwtRequest</url-pattern> </servlet-mapping>
Without this servlet mapping configuration, we would receive "HTTP 404 - Not Found" error when attempting to make a GWT request from our console.html file. Note that all mapping elements, including this servlet mapping element, have to be placed after the corresponding declaration, which is the requestFactory servlet declaration in this case.
- To avoid a runtime exception, we need to handle the new SC_FORBIDDEN error code returned by GaeAdminFilter in the case of request denial. This will be done by adding the following block to handle the new error code in createRequestCallback() method of ~.client.scaffold.gae.GaeAuthRequestTransport.
if (Response.SC_FORBIDDEN == statusCode) { String loginUrl = response.getHeader("login"); if (loginUrl != null) { receiver.onTransportFailure(new ServerFailure( "Forbidden content", null, null, false /* not fatal */)); eventBus.fireEvent(new GaeAuthenticationFailureEvent(loginUrl)); return; } }
At this point, our new HTML host page is only accessible by admin users but displays the same content as the original host page. You should be able to run your application and verify that admin users can view both host pages, whereas normal users will be redirected to the login screen when attempting to access console.html.
The next task on our to-do list is to change the content that is displayed by each host page. We want the new console.html to display the original scaffold application, which we intend to use as an administration console, and the ApplicationScaffold.html to display new content.
- Lets start by creating a new ~.client.custom.ui.CustomView using UiBinder GWT wizard, which will create a CustomView.ui.xml and a CustomView.java file. CustomView.ui.xml will be a copy of ScaffoldDesktopShell.ui.xml without the entity list and entity details elements and their corresponding style declarations.
<!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" xmlns:s="urn:import:com.blogspot.gwtsts.pizzashop.client.scaffold.ui"> <ui:image field='gwtLogo' src="../../style/images/gwtLogo.png"/> <ui:image field='rooLogo' src="../../style/images/rooLogo.png"/> <ui:style> ... </ui:style> <g:DockLayoutPanel unit='EM'> <g:north size='6'> <g:HTMLPanel styleName='{style.centered}'> <div class='{style.banner}'> <div class='{style.error}' ui:field='error'></div> <span class='{style.title}'> <h2>Pizza Shop</h2> </span> <s:LoginWidget styleName='{style.login}' ui:field="loginWidget"/> </div> </g:HTMLPanel> </g:north> <g:south size='2'> <g:HTML> <div class='{style.logos}'> <span>Powered by:</span> <a href='http://code.google.com/webtoolkit/'> <div class='{style.gwtLogo}'></div> </a> <a href='http://www.springsource.org/roo/'> <div class='{style.rooLogo}'></div> </a> </div> </g:HTML> </g:south> <g:center> <g:HTML> <h1 style='text-align: center'>Welcome to the Pizza Shop!</h1> </g:HTML> </g:center> </g:DockLayoutPanel> </ui:UiBinder>
We will need to add LoginWidget field and its accessor method to CustomView.java to give us the following.
public class CustomView extends Composite { private static CustomViewUiBinder uiBinder = GWT .create(CustomViewUiBinder.class); @UiField LoginWidget loginWidget; interface CustomViewUiBinder extends UiBinder<Widget, CustomView> { } public CustomView() { initWidget(uiBinder.createAndBindUi(this)); } /** * @return the login widget */ public LoginWidget getLoginWidget() { return loginWidget; } }
- We now have to return to our console.html host page to find a way to differentiate it from ApplicationScaffold.html. The span tag for the "loading" message is a perfect candidate for this. Setting the id attribute of the span tag to something that differs from the one in ApplicationScaffold.html will allow us to differentiate the two host pages.
<span id='loadingConsole' style="..."> Loading Admin Console… </span>
- We will update ~client.scaffold.ScaffoldDesktopApp's run() method to check if the host page contains an element with id attribute equal to loading or loadingConsole. This will allow us to determine which host page we are dealing with.
public void run() { /* Add handlers, setup activities */ init(); LoginWidget loginWidget = null; Widget mainWidget = null; Element loading = Document.get().getElementById("loading"); if (loading != null) { CustomView customView = new CustomView(); loginWidget = customView.getLoginWidget(); mainWidget = customView; loading.getParentElement().removeChild(loading); } Element loadingConsole = Document.get().getElementById("loadingConsole"); if (loadingConsole != null) { loginWidget = shell.loginWidget; mainWidget = shell; loadingConsole.getParentElement().removeChild(loadingConsole); } // AppEngine user authentication new GaeLoginWidgetDriver(requestFactory).setWidget(loginWidget); /* And show the user the shell */ RootLayoutPanel.get().add(mainWidget); }
Notice that the LoginWidget initialization by GaeLoginWidgetDriver has moved from the init() method to the run() method as the instance of LoginWidget to be initialized depends on the host page being accessed.
At this point, our new HTML host pages are displaying different content. The original scaffold view has become our administration console, accessible only by admin users through admin/console.html, and our new view is accessible by all registered users through ApplicationScaffold.html.
Now, lets link the two pages to each other by adding new Anchor widgets to both host pages. We want a link to the administration console that is only visible to users with administrative privileges on CustomView and a link that will return admin users back to the CustomView on the administration console (ScaffoldDesktopShell).
- We will start by creating a new ~.client.scaffold.ui.ReturnLinkWidget using the UiBinder GWT wizard. ReturnLinkWidget will be simple and contain only one Anchor widget styled the same way as LoginWidget for consistency.
<!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> .link { /* make it look like text */ color: inherit; text-decoration: inherit; } </ui:style> <g:HTMLPanel> <div> <g:Anchor ui:field="returnLink" addStyleNames="{style.link}">Pizza Shop</g:Anchor> </div> </g:HTMLPanel> </ui:UiBinder>
Adding an Anchor field and a setReturnUrl() method to ReturnLinkWidget.java will give us the following.
public class ReturnLinkWidget extends Composite { private static ReturnLinkWidgetUiBinder uiBinder = GWT .create(ReturnLinkWidgetUiBinder.class); @UiField Anchor returnLink; interface ReturnLinkWidgetUiBinder extends UiBinder<Widget, ReturnLinkWidget> { } public ReturnLinkWidget() { initWidget(uiBinder.createAndBindUi(this)); } public void setReturnUrl(String url) { returnLink.setHref(url); } }
- We will also need a driver for our new widget in the form of ~.client.console.ReturnLinkWidgetDriver which provides a setWidget() method.
public class ReturnLinkWidgetDriver { public void setWidget(final ReturnLinkWidget widget) { UrlBuilder urlBuilder = Window.Location.createUrlBuilder(); urlBuilder.setPath("/ApplicationScaffold.html"); widget.setReturnUrl(urlBuilder.buildString()); } }
- Having accessed GWT's Window module, we are going to have to inherit it in our module definition (ApplicationScaffold.gwt.xml) file by inserting the following line.
<inherits name="com.google.gwt.user.Window"/>
- Now that we have created ReturnLinkWidget, we can add it to our administration console view, ~.client.scaffold.ScaffoldDesktopShell. Adding the ReturnLinkWidget and assigning it a custom style for placement will give us the following ScaffoldDesktopShell.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' xmlns:s='urn:import:com.blogspot.gwtsts.pizzashop.client.scaffold.ui'> <ui:image field='gwtLogo' src="../style/images/gwtLogo.png"/> <ui:image field='rooLogo' src="../style/images/rooLogo.png"/> <ui:style> @def contentWidth 850px; ... .returnLink { position: absolute; left: 75%; right: 0%; top: 50%; text-align: center; color: #def; } ... </ui:style> <g:DockLayoutPanel unit='EM'> <g:north size='6'> <g:HTMLPanel styleName='{style.centered}'> <div class='{style.banner}'> ... <s:ReturnLinkWidget styleName='{style.returnLink}' ui:field="returnLinkWidget"/> </div> </g:HTMLPanel> </g:north> <g:south size='2'> ... </g:south> <g:center> ... </g:center> </g:DockLayoutPanel> </ui:UiBinder>
Additionally, we will need to add the ReturnLinkWidget field and accessor method to ScaffoldDesktopShell.java.
public class ScaffoldDesktopShell extends Composite { ... @UiField ReturnLinkWidget returnLinkWidget; ... /** * @return the login widget */ public ReturnLinkWidget getReturnLinkWidget() { return returnLinkWidget; } ... }
- One last modification we need to make before ReturnLinkWidget is fully operational is adding a call to ReturnLinkWidgetDriver's setWidget() method for the case when the admin/console.html host page is being accessed in ~.client.scaffold.ScaffoldDesktopApp's run() method.
public void run() { ... Element loadingConsole = Document.get().getElementById("loadingConsole"); if (loadingConsole != null) { loginWidget = shell.loginWidget; mainWidget = shell; loadingConsole.getParentElement().removeChild(loadingConsole); new ReturnLinkWidgetDriver().setWidget(shell.returnLinkWidget); } ... }
You should now be able to see this new link by accessing admin/console.html as an admin user. Clicking the link should take you back to the CustomView.
We need to create one last widget that links admin users to the administration console before we can start selling pizzas online; however, this task is not going to be as easy as ReturnLinkWidget, because we have the requirement of making the link visible only for admin users. In order to accomplish this, we need to know if users have admin privileges on the client side, which means that we will be creating a new service request method.
- Lets get started right away by updating ~.shared.gae.GaeUserServiceRequest with our service method declaration.
@Service(value = UserServiceWrapper.class, locator = UserServiceLocator.class) public interface GaeUserServiceRequest extends RequestContext { ... public Request<Boolean> isUserAdmin(); }
- Next step is to add a declaration to ~.server.gae.UserServiceWrapper interface.
public interface UserServiceWrapper { ... public Boolean isUserAdmin(); }
- Final step is to add the implementation to ~.server.gae.UserServiceLocator.
public class UserServiceLocator implements ServiceLocator { public UserServiceWrapper getInstance(Class<?> clazz) { final UserService service = UserServiceFactory.getUserService(); return new UserServiceWrapper() { ... @Override public Boolean isUserAdmin() { return new Boolean(service.isUserAdmin()); } }; } }
We are now ready to create the ConsoleLinkWidget and add it to CustomView.
- Lets use the UiBinder GWT wizard to create ~.client.custom.ui.ConsoleLinkWidget. ConsoleLinkWidget.ui.xml will be very similar to ReturnLinkWidget.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> .link { /* make it look like text */ color: inherit; text-decoration: inherit; } </ui:style> <g:HTMLPanel> <div> <g:Anchor ui:field="consoleLink" addStyleNames="{style.link}">Admin Console</g:Anchor> </div> </g:HTMLPanel> </ui:UiBinder>
- An Anchor field and a setConsoleUrl() method will be added to ConsoleLinkWidget.java to produce the following.
public class ConsoleLinkWidget extends Composite { private static ConsoleLinkWidgetUiBinder uiBinder = GWT .create(ConsoleLinkWidgetUiBinder.class); @UiField Anchor consoleLink; interface ConsoleLinkWidgetUiBinder extends UiBinder<Widget, ConsoleLinkWidget> { } public ConsoleLinkWidget() { initWidget(uiBinder.createAndBindUi(this)); this.setVisible(false); } public void setConsoleUrl(String url) { consoleLink.setHref(url); } }
Notice that we have added a call to Widget's setVisible() method in the constructor. This is because we want the Widget to be invisible by default.
- As was the case for ReturnLinkWidget, next step is to create a widget driver that implements a setWidget() method.
package com.blogspot.gwtsts.pizzashop.client.console; ... public class ConsoleLinkWidgetDriver { private final MakesGaeRequests requests; public ConsoleLinkWidgetDriver(MakesGaeRequests requests) { this.requests = requests; } public void setWidget(final ConsoleLinkWidget widget) { GaeUserServiceRequest request = requests.userServiceRequest(); request.isUserAdmin().to(new Receiver<Boolean>() { public void onSuccess(Boolean response) { if (response.booleanValue() == true) { UrlBuilder urlBuilder = Window.Location.createUrlBuilder(); urlBuilder.setPath("/admin/console.html"); widget.setConsoleUrl(urlBuilder.buildString()); widget.setVisible(true); } else { widget.setVisible(false); } } }); request.fire(); } }
- Time has come to add ConsoleLinkWidget to the new ~.client.custom.ui.CustomView. Adding the ConsoleLinkWidget and assigning it a custom style for placement will give us the following CustomView.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" xmlns:s="urn:import:com.blogspot.gwtsts.pizzashop.client.scaffold.ui" xmlns:t="urn:import:com.blogspot.gwtsts.pizzashop.client.custom.ui"> <ui:image field='gwtLogo' src="../../style/images/gwtLogo.png"/> <ui:image field='rooLogo' src="../../style/images/rooLogo.png"/> <ui:style> @def contentWidth 850px; ... .console { position: absolute; left: 75%; right: 0%; top: 50%; text-align: center; color: #def; } ... </ui:style> <g:DockLayoutPanel unit='EM'> <g:north size='6'> <g:HTMLPanel styleName='{style.centered}'> <div class='{style.banner}'> ... <t:ConsoleLinkWidget styleName='{style.console}' ui:field="consoleLinkWidget"/> </div> </g:HTMLPanel> </g:north> <g:south size='2'> ... </g:south> <g:center> ... </g:center> </g:DockLayoutPanel> </ui:UiBinder>
Additionally, we will need to add the ConsoleLinkWidget field and accessor method to CustomView.java.
public class CustomView extends Composite { ... @UiField ConsoleLinkWidget consoleLinkWidget; ... /** * @return the login widget */ public ConsoleLinkWidget getConsoleLinkWidget() { return consoleLinkWidget; } }
- Finally, lets add the code that initializes the ConsoleLinkWidget to ~.client.scaffold.ScaffoldDesktopApp's run() method.
public void run() { ... Element loading = Document.get().getElementById("loading"); if (loading != null) { CustomView customView = new CustomView(); loginWidget = customView.getLoginWidget(); mainWidget = customView; loading.getParentElement().removeChild(loading); new ConsoleLinkWidgetDriver(requestFactory).setWidget(customView.getConsoleLinkWidget()); } ... }
We have achieved all our objectives at this point. Only admin users are able to see a link that takes them to the admin console at this point. They are able to follow this link to the admin console and follow the return link back to the main view.
Source Code
I have zipped up and uploaded the source directory for the Pizza Shop project. The zip file includes all changes made in this post and can be downloaded via this link.
Uploaded To App Engine
I have also deployed the current state of the project to App Engine and it can be accessed via http://1.pizzashopexample.appspot.com. IE9 users might have problems launching the application.
Deploying to App Engine is very easy. Sign up to App Engine, register an application, and enter application ID in appengine-web.xml prior to deploying. Then select "Google > Deploy to App Engine" from project's context menu, enter Google username and password, and you are done!
In The Next Post
We will be creating a new GWT module and linking two modules to each other.
Intro | Part I | Part II | Part III | Part IV | Part V |
Also check out The Unofficial Google Web Toolkit Blog
Labels:
App Engine,
GAE,
Google,
GWT,
Rapid Application Development,
Roo,
SpringSource,
STS,
Web Application Development
Subscribe to:
Posts (Atom)