Home Thoughts Thought Articles The Three Amigos - Maven, Spring and GWT
The Three Amigos - Maven, Spring and GWT
Written by Stephen Callaghan   
Thursday, 20 March 2008 00:00
Article Index
The Three Amigos - Maven, Spring and GWT
Setup and Organization
Spring Services
Google Web Toolkit
All Pages

Service-Oriented Architecture (SOA) is a widely used - and misused - phrase in IT, having evolved to have different meanings to different people.

For some it is no more than a re-branding of an architectural approach used for decades on large scale enterprise applications. Examples include CORBA Services in the 1990s, RMI from Java 1.1 onwards and DCOM.

For others, SOA is a new nirvana; a place where where corporate services are exposed via Web Services to be discovered and consumed by a multitude of clients (using any technology), the end-result being rich and productive new systems built from standard building blocks. To quote Wikipedia :

"SOAs build applications out of software services. Services are relatively large, intrinsically unassociated units of functionality, which have no calls to each other embedded in them."

Whatever the merits of the two camps, when designing and building large corporate systems there is no doubt that this design practice is a good thing. Without it, systems would not scale, applications would constantly be re-inventing the same wheel, and things would very quickly devolve into a maintenance nightmare.

What has changed with SOA in recent times is the technology - much of which is now freely available. In the past, a decision on SOA typically involved hundreds of thousands of licensing dollars. This is no longer necessarily the case. Furthermore, solutions are now quicker and easier to assemble, with systems increasingly being built upon standard well-known building blocks. Examples of this include HTTP-based protocols and standard application containers like Tomcat.

In parallel to the rise of affordable SOAs, new web-based technologies have matured to provide a new path for building feature rich Web 2.0 Ajax-styled user interfaces. Coming from an open source and Java background, two in particular have been on our radar, namely Ruby on Rails (RoR) and the Google Web Toolkit (GWT). They both have radically different approaches to solving the issue of Web 2.0 development.

While RoR delivers a complete end-to-end solution from the database to the user interface (and also happens to use a relatively new language), GWT is purely a web-tier solution where all development is done in Java. This point of difference - that GWT can leverage existing Java-based services and is only concerned as a web tier solution - makes it ideally placed to be used in a mature corporate IT environment.

This article will show how to build a SOA-based system using Spring Services, a GWT client to talk to those services, and Maven 2 as a build tool to hold it altogether. There is also an associated reference project, created here at Shine Technologies and now open-sourced on Google Code using the Apache 2 License. Full project details, including fully downloadable source (including Eclipse projects) are available at http://shine-reference.googlecode.com.

This project can be used to easily navigate and run the following examples, and could potentially be used as a basis for quickly getting your own project up-and-running. What's more, the project will be maintained and updated as feedback and enhancements arrive both from teams within Shine and the IT community at large.

This article will start by covering the setup of a build environment using Maven. Then we'll start working through each of the layers in sequence, beginning with getting up-and-running with a compatible Spring Service layer. Next we'll add a bridge between GWT and the Spring Services. Finally, we'll introduce a GWT client that can attach to the bridging layer and thus communicate with the Spring services.


Maven 2, Setup and Organization

Maven has evolved as an important tool for Java development projects, providing both build automation and project management infrastructure. It has a couple of key benefits:

  • Dependency Management: Maven handles all dependency management by automatically downloading dependencies on demand, storing them in a local repository and sharing them amongst any projects that require them. In our Eclipse projects, the classpath includes references to this local repository, thus eliminating the need to have duplicate copies of common libraries in each project.
  • Standardized Project Structure: Artifacts like source code, property files and JUnit tests all have a standard location in the project. Put bluntly, if you understand one Maven project structure, you can pretty much can understand any Maven project.

That said, Maven has a steep learning curve. Whereas with Ant it is easy to get started and hard to do complicated tasks, Maven is the opposite. The online documentation is still poor, but once it's running it becomes increasingly easy to do complicated tasks. For example, we will see in Spring Services that mvn jetty:run-war compiles, builds, deploys and runs the service layer in a Jetty container - with no handcrafted code required.

Each Maven project has a pom.xml file that defines it. Although this can be quite verbose XML, Eclipse plugins are available (for example, m2eclipse) that understand the format and take away some of the pain of dealing with it.

Setup

The first thing you should do is check to see if you have Maven 2 installed. Some operating systems (for example, Leopard) have Maven 2 installed by default. To find out if you've got it, enter onto the command-line:

mvn --version

If it says 2.0.x or later, you're in luck. If not, follow the instructions from the Maven site to install it.

Once installed, we need to enable Maven for GWT. GWT support for Maven is still pretty much in its infancy, so the best we can do is follow the manual setup procedures from the Maven GWT Project. This requires a separate download of the GWT Toolkit outside of Maven. However, this is still relatively straightforward to do:

  1. Download the GWT Toolkit and install it. This is usually as simple as unzipping it. On a Mac, for example, you might unzip it to /Applications/java/gwt-mac-1.4.61
  2. Add a profile to your Maven settings.xml file (if you are using the default local repository location, this file will be under your home directory at .m2/settings.xml) and point the profile to where you installed the toolkit. It'll end up looking something like this:
<settings>
<profiles>
<profile>
<id>gwt-1.4.61</id>
<properties>
<google.webtoolkit.home>/Applications/java/gwt-mac-1.4.61</google.webtoolkit.home>
<!-- XstartOnFirstThread needed only on the mac -->
<google.webtoolkit.extrajvmargs>-XstartOnFirstThread</google.webtoolkit.extrajvmargs>
</properties>
</profile>
</profiles>
<activeProfiles>
<activeProfile>gwt-1.4.61</activeProfile>
</activeProfiles>
</settings>

Project Setup

Now that we've set up Maven, we'll set up the project. We'll actually split the build into three separate layers, a service tier, a common interface, and the GWT client.

Download and unpack the zip file containing full project in the Downloads Tab from the Shine Reference Site. Alternately, you can checkout a read-only copy of the project using Subversion:

svn checkout http://shine-reference.googlecode.com/svn/trunk/shine-reference shine-reference-read-only

This then gives us three sub-projects (as shown below), each with its own separate pom.xml, and each with it's own separate Eclipse project:

maven-structure


Spring Services

Background

For the last 2 years we have helped one of our clients upgrade their existing legacy system (a combination of COBOL and Oracle) to use a modified version of SOA. The infrastructure and key software components are best described with a diagram:

SOA_Infrastructure

In implementing a solution on this infrastructure, we have taken the essence of SOA and distilled into the following concepts:

  • All Services are Stateless and Atomic. This allows scalability by simply adding on new hardware and/or instances.
  • All Services built from open source technologies. Our experience shows that open source solutions have matured to a level where they can compete with commercial applications in the Enterprise space.
  • There is loose coupling between the service tier and any business clients (web sites, phone IVR systems, web services etc) based on the Service signature interface and the Data Transfer Object (DTO)
  • The implementation uses Java and Spring Remote Services. By using Spring, these services can be exposed via a variety of transport mechanisms (for example, Hessian, Burlap, or RMI) and linked to various clients depending on requirements.
  • Exposed methods are more granular than traditional SOA.
  • Services are not envisaged to be used by unknown clients. Thus a service-discovery mechanism is not a requirement.
  • Services are based on distinct business functions. For example, there is an MemberService service with operations such as getMemberDetails(memberNumber).

Service Common Jar Interface

To achieve the loose coupling from services to clients, we created a "service-common.jar" that contains the Spring Service signatures and the Data Transfer Objects (DTOs). In this example we have created a simple Member Details service and associated DTO to hold the data.

public interface IMemberDetailsService
{
MemberDTO getMemberDetails(String memberNumber);
void saveMemberDetails(MemberDTO member);
}
public class MemberDTO implements Serializable
{
static final long serialVersionUID = 5591581822321437717L;

private String firstName;
private String lastName;

public String getFirstName() { return firstName;}
public void setFirstName(String firstName) { this.firstName = firstName;}

public String getLastName() { return lastName;}
public void setLastName(String lastName) { this.lastName = lastName;}
}

There is one addition that needs to be made to this shared JAR - and this is where the GWT magic happens. You need to add in an XML fragment that defines the DTO's as a loadable module - this will become clear in the next section but basically it allows the DTOs to be used unmodified and compiled into the GWT Javascript. The fragment lives at com/shinetech/sample/common/GWTShineCommon.gwt.xml and looks like this:

<!-- GWT Module Definition -->
<module>
<source path="dto"/>
</module>

Additionally, to allow the DTOs to be compiled into the GWT Client, there are a couple of restrictions on the common jar that need to be adhered to:

  • No Annotations. Currently the GWT compiler will fail if these are detected.
  • Keep return types as arrays, not collections. For example you'd use:
    public MemberDetails[] getAllMemberDetails()

    instead of:

    public List<MemberDetails> getAllMemberDetails()
  • Use serialVersionUID to allow cosmetic changes to your DTOs without the need to deploy and retest all clients.

These limitations are largely due to GWT's lack of support for Java 5 and mean that the DTOs are very plain POJOs. Nevertheless, following these principles you will have a service common jar that can be used by many other systems to connect to your Spring Services.

SOA_Build_Process

To build and install the service common project, change directory to shine-reference/shine-reference-common and run the following:

mvn source:jar install

This will build and deploy the common jar to the local Maven repository, including a copy of the source code which will be needed by the GWT layer.

We'll now move onto the Spring Services themselves.

Spring Service Infrastructure

Spring Services are very easy to setup. We have already completed the first step - defining the Service as a Java Interface and packaging it into a common jar. Note that the service definition contains no Spring code or inherited classes on the interface. This keeps it very clean and loosely coupled. The second step is to implement this service. Our implementation is trivial, but you can use more sophisticated technologies (for example, Hibernate) as needed. Here's what it looks like:

/* Service implementation of the IMemberDetailsService as
defined in the service common jar. */
public class MemberDetailsServiceImpl implements IMemberDetailsService
{
private static final Log LOG = LogFactory.getLog(MemberDetailsServiceImpl.class);
private static MemberDTO sampleMember;

public MemberDetailsServiceImpl()
{
LOG.info("Creating Service, creating sample member '1'...");
sampleMember = new MemberDTO();
sampleMember.setFirstName("Stephen");
sampleMember.setLastName("Callaghan");
}


public void saveMemberDetails(MemberDTO member)
{
LOG.info("Updating Sample Member");
sampleMember = member;
}


public MemberDTO getMemberDetails(String memberNumber)
{
if ((memberNumber != null) && (memberNumber.trim().equals("1")))
{
LOG.info("Returning sample member '1'");
return sampleMember;
}

LOG.info("Member not found : " + memberNumber);
return null;
}
}

Next, we expose the service using Spring Remoting. Available transport protocols include RMI, Spring's HTTP Invoker, Hessian, Burlap, JAX-RPC, JAX-WS and JMS. The important thing is that the protocol to use is set in the Spring configuration and that the Java classes contain no knowledge whatsoever of the protocol used.

For HTTP-based transports, a dispatcher servlet must be defined as well in web.xml:

!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<!--
// web.xml
// Loads Spring Context and sets up Dispatcher Servlet for Spring
-->
<web-app>
<display-name>Shine Spring Sample</display-name>

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/classes/springconfig-service.xml</param-value>
</context-param>

<!-- Loads and makes available the context defined in springconfig-service.xml -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- Dispatcher Servlet catches the remote requests -->
<servlet>
<servlet-name>service</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<!-- All urls /*.service are mapped to the dispatcher. -->
<servlet-mapping>
<servlet-name>service</servlet-name>
<url-pattern>*.service</url-pattern>
</servlet-mapping>

</web-app>

The underlying service bean is then defined in the Spring application context file springconfig-service.xml as follows:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd"
default-autowire="byName">

<context:annotation-config/>

<!-- Defines the Member Details Service bean -->
<bean id="memberDetailsService"
/>
</beans>

Finally, for HTTP-based remoting we expose this service in service-servlet.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<!--
service-servlet.xml
Application context definition for "service" DispatcherServlet.
Exposes service interfaces. For implementations, see springconfig-service.xml
-->
<beans>
<!-- Link the url to the bean implementation -->
<bean id="httpInvokerMemberDetailsService"
>
<property name="service" ref="memberDetailsService"/>
<property name="serviceInterface" value="com.shinetech.sample.common.IMemberDetailsService"/>
</bean>

<!-- Define URL for Service /memberDetailsService.httpInvoker.service -->
<bean id="urlMapping">
<property name="mappings">
<props>
<prop key="/memberDetailsService.httpInvoker.service">httpInvokerMemberDetailsService</prop>
</props>
</property>
</bean>

</beans>

To install & run the service project, change directory to the shine-reference/shine-reference-spring and run:

mvn jetty:run-war

This will start a service running that is available at http://localhost:8080/Shine-Spring-Sample. Note that, depending on the status of your Maven repository, it may take a while to start on the first attempt - a number of Jars may need to be downloaded, especially to start the Jetty container.


Google Web Toolkit

Creating a Simple Screen

Getting started with GWT is straight-forward as the vast majority of the development is in standard Java. At a very high level, all Java code is compiled into compatible JavaScript and is accessed by a web page via an entry point. All browser compatibility issues are handled by the GWT compiler. There is no need to get into the details of what goes on in the Javascript at all, in fact you can write robust production-ready AJAX code without knowing a single thing about Javascript - all of it is handled by GWT (Although you can write your own Javascript snippets if you really have to).

Before starting, there are a couple of important design principles to understand about GWT:

  • GWT pages are created in a very similar way to designing Swing based applications. ie. You create a TextField, add an event handler, do something on the firing of an event, etc.
  • All client side Java code is "compiled / converted" to HTML and Javascript
  • All remote service calls are handled asynchronously (and this is where a little bit of knowledge of basic AJAX does come in useful).

For more information, take a look at the GWT Getting Started Site.

Let's first create a simple Member Screen in SampleApp.java:

public class SampleApp implements EntryPoint
{
private Button btnSearch = new Button("Search");
private TextBox tbSearch = new TextBox();
private TextBox tbFirstName = new TextBox();
private TextBox tbLastName = new TextBox();

public void onModuleLoad()
{
// Create the UI to display
// Note, similarity to Swing Coding
FlexTable flexTable = new FlexTable();
btnSearch.addClickListener(new ClickListener()
{
public void onClick(Widget sender)
{
// Add listener but do nothing just now
//callMemberSearch();
}
});
flexTable.setWidget(0, 0, btnSearch);
flexTable.setText(1, 0, "First Name");
flexTable.setText(2, 0, "Last Name");
flexTable.setWidget(0, 1, tbSearch);
flexTable.setWidget(1, 1, tbFirstName);
flexTable.setWidget(2, 1, tbLastName);

HorizontalPanel panel = new HorizontalPanel();
panel.add(flexTable);

// Add this UI to defined "slot1" on the HTML page
RootPanel.get("slot1").add(panel);
}
}

Once compiled and run it'll look something like this:

screen-memberdetails

A possible enhancement here is to replace the basic widget set that comes with GWT. One such solution is GWT-ext which builds upon the standard EXT Javascript toolkit and provides a very slick look and feel, plus additional functionality such as form based validation.

Service Calls with GWT

Obviously, the above code needs to connect and obtain data. This is achieved by making an asynchronous call to a Java Servlet that is part of the GWT Web Tier deployment. This then connects back to the deployed sample Spring Service we built earlier. As a first step we want to use the service common Jar to access the same DTOs the remote service uses. Add the following line into the GWT project definition file SampleApp.gwt.xml:

<inherits name="com.loyaltypacific.flybuys.common.GWTCommon"/>

This is a reference to the Jar that was created in the Spring project (note that it needs access to both the class and the source Jar. Fortunately, Maven will take care of this for us) and its GWT XML reference in com.loyaltypacific.flybuys.common.GWTCommon.gwt.xml.

Next, create the GWT RPC Services, starting with the service that will be used by the GWT Servlet:

public interface IMemberDetailsGWTService extends RemoteService
{

MemberDTO getMemberDetails(String memberNumber);
void saveMemberDetails(MemberDTO member);

}

Then create the Asynchronous Interface that will be used by the GWT client code:

public interface IMemberDetailsGWTServiceAsync
{

void getMemberDetails(String memberNumber, AsyncCallback callback);
void saveMemberDetails(MemberDTO member, AsyncCallback callback);

}

Client-side GWT does not currently support Java 5 and only has a limited subset of available APIs, this limitation only applies to the "Javascript Client". The server-side of the GWT RPC interface can run in any Java environment. The following UML diagram shows where each of the interfaces created above is used.

SOA_GWT_RPC_UML

GWT Client Service Coding

The GWT Client (SampleApp.java) is then extended to include the GWT RPC call. Whilst this method may appear quite verbose, it is possible to create a cut-down version. However, we have shown in its entirety to expose all the steps:

    private void callMemberSearch()
{
// Setup Service Endpoint
IMemberDetailsGWTServiceAsync svcMemberDetailsAsync =
(IMemberDetailsGWTServiceAsync)GWT.create(IMemberDetailsGWTService.class);
ServiceDefTarget endpoint = (ServiceDefTarget)svcMemberDetailsAsync;
endpoint.setServiceEntryPoint(GWT.getModuleBaseURL() + "service");

// Create Callback
AsyncCallback callback = new AsyncCallback()
{
// Called if successful
public void onSuccess(Object result)
{
// Cast result to known result class
MemberDTO member = (MemberDTO)result;
if (member == null)
{
tbSearch.setText("Not Found");
}
else
{
tbFirstName.setText(member.getFirstName());
tbLastName.setText(member.getLastName());
}
}

// Called if unsuccessful
public void onFailure(Throwable caught)
{
GWT.log("Exception calling service", caught);
}
};

// Call Service
svcMemberDetailsAsync.getMemberDetails(tbSearch.getText(), callback);
}
}

Note that the Spring DTOs defined in the Common Service Jar are used unchanged in the client code to set the text fields - very handy.

GWT Server Service Coding

To catch the asynchronous call, a new servlet must be created that implements the service interface (SampleServiceImpl.java). This class is a standard Java class that will be hosted within a container (like Tomcat) and not compiled down to JavaScript.

The steps for creating this servlet are as follows:

  • Extend the GWT Servlet class com.google.gwt.user.server.rpc.RemoteServiceServlet.
  • Implement the non-asynchronous verison of the RPC service IMemberDetailsGWTService.
  • On initialization, load the Spring Context and then the Spring Member Details Service as defined in Section 1 and shared by the service common jar.

In our example, the new servlet ends up looking like this:

public class SampleServiceImpl extends RemoteServiceServlet
implements IMemberDetailsGWTService
{
private IMemberDetailsService svcMemberDetails;

public void init(ServletConfig config) throws ServletException
{
super.init(config);
// Load the Spring Application Context
ApplicationContext context = new ClassPathXmlApplicationContext(
"springconfig-remote-services.xml");
// Retrieve Member Details Service
svcMemberDetails = (IMemberDetailsService)
context.getBean("memberDetailsService");
}

public MemberDTO getMemberDetails(String memberNumber)
{
return svcMemberDetails.getMemberDetails(memberNumber);
}

public void saveMemberDetails(MemberDTO member)
{
svcMemberDetails.saveMemberDetails(member);
}

}

To define the Remote Services to Spring, the application context in springconfig-remote-services.xml has the following details:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd"
default-autowire="byName">

<context:annotation-config/>

<!-- Loads the property file holding the remote server properties -->
<bean id="propertyConfigurer"
>
<property name="locations">
<list>
<value>classpath:remoteserver.properties</value>
</list>
</property>
</bean>

<!-- Defines the Member Details Service and how to contact it -->
<bean id="memberDetailsService"
>
<property name="serviceUrl">
<value>http://${serverName}/Shine-Spring-Sample/memberDetailsService.httpInvoker.service</value>
</property>
<property name="serviceInterface">
<value>com.shinetech.sample.common.IMemberDetailsService</value>
</property>
</bean>

</beans>

Finally, we modify remoteserver.properties to contain the location of the services:

#
# Location of the Spring Services
#
serverName=127.0.0.1:8090

You could alternatively use JNDI here if deploying to an application container like Tomcat. The property file shown here is the simplest possible solution.

Now we'll leverage the GWT Maven Project to do the heavy lifting of building and running this GWT client. Change directory to shine-reference/shine-reference-gwt and run the command:

mvn gwt:gwt

This will build and deploy the GWT client into hosted mode, making it available at http://localhost:8888/com.shinetech.SampleApp/SampleApp.html. Searching on ID '1' should give the result as shown below

SOA_-_Running

Drawbacks

The most obvious drawback with this GWT/Spring solution is the need to redefine the Service interfaces code the pass-through Servlet which routes the GWT RPC calls through to the Spring backend. This is largely because of the need for the non-asynchronous service to extend com.google.gwt.user.client.rpc.RemoteService. A much cleaner alternative would be to be able to reuse the interface as packaged in the pre-built common.jar.

There are a number of projects and solutions that are attempting to solve this problem; I particularly like leveraging the new Spring 2.5 features as detailed here However, in the production environment where we implemented our solution it actually turned out to be somewhat beneficial to have to code the 2 new interfaces. The reasons for this were:

  • It was not desirable to expose all the available Spring Services from a security point of view, only a limited subset were deemed necessary and even there, additional security measures were put in place.
  • Due to the screen design of the GWT application, the GWT RPC calls usually turned out to be variations and supersets of the underlying Spring Services. For example, GWT RPC getMemberDetails() turned out to be a superset of the Spring Service calls getMemberDetailsCore() and getMemberAddressDetails() - these two calls were joined together in the GWT RPC Servlet.
  • Coding of the GWT Client could be performed in parallel without tight reference to the Spring services. This speeded-up development and did not constrain the client design decisions.
  • The overhead of maintaining the two extra service interfaces was not large, as the majority of the coupling between the GWT and Spring is in the DTOs - which were able to be used completely unchanged (and ultimately this is the big advantage of not having to code a separate web tier model).

Summary

After reading this article and running the Shine Reference Project you should be able to get your own Spring/GWT project up-and-running quickly. Whilst this is not a single-tier rapid solution to get a quick website up, it is an architecture that splits re-useable services into a separate layer and then bolts on a GWT application to consume those services (in addition to any other type of client that may need to). If a single tier was needed, many of the design decisions would have been radically different.

We have successfully implemented a variation of this solution for one of our clients to wrap existing legacy COBOL services and an Oracle database in a Spring Layer. Initially, this was used by a number of different client systems, ranging from traditional websites to phone IVRs. However, we were then able to re-use this service layer (completely unchanged) with a new GWT Admin Client, providing our client with a migration path from existing COBOL Admin screens to a solid, feature rich and inexpensive Web 2.0 solution. In a commercial environment this incremental improvement that GWT provides (being Java based) can be far less of a risk than completely switching technologies to a new platform.

It is my conclusion that GWT is a profound shift in Web Application development in the Java space. GWT is, in my opinion, easier to learn than other frameworks like JSF, Wicket or even Struts. Indeed, once you have used GWT it is very hard to go back. It integrates into any existing Java service layer, and since Java itself now has a strong history of integrating into legacy environments, the combination makes a very promising solution to the problem of putting modern Ajax web interfaces onto legacy systems.

As consultants we believe in providing the appropriate solution to the problem at hand, and in many cases GWT will be a strong contender. And finally, with a company like Google behind it, this is an option that will not be disappearing anytime soon.

Web Resources