Thursday, April 10, 2014

Host a Soap Web Service on Google App Engine with JAX-WS

We are about to release a great addon for JDeveloper that can access a SOAP web service. For demonstration purposes we want to have a publicly available web service that anyone can use. Having this web service hosted on Google App Engine has two major benefits: it is free and is accessible for anyone on the internet 24x7.

We wanted to implement a simple Java (JAX-WS) webservice on Google App Engine, but unfortunately this is not fully supported. All the javax.xml.* classes are available on Google App Engine, but not the com.sun.xml.ws.* classes that are normally used to implement a JAX-WS service. But with a little bit of custom code we can get a JAX-WS service to run on Google App Engine as can be seen in the SoapUI screenshot. You can download the public WSDL at https://redheap-jaxws.appspot.com/HelloWorldService.wsdl
SoapUI test invoking HelloWorld service on Google App Engine

You can use the normal JAX-WS annotations in your service class and use JAXB to marshal and unmarshal the request and responses. This is all very similar to a JAX-WS service on a JEE container. The thing you need extra is a custom HttpServlet that handles the HTTP POST, unmarshals the request payload to a java object using JAXB, invoke the actual web service class, marshal the web service class response back to XML and send it to the client.

This post describes all the steps to create a project using maven and complete it in JDeveloper 12c as well as deploying it to a local and remote server. If you prefer other build tools, like Apache ANT, or other Java IDE's you can still use similar steps but you might need to adjust them for your environment. The key part is the custom servlet which is the same for each setup.

Navigate to the directory where you want to create a subdirectory for a project. No need to create the project directory yourself, just navigate to its parent. Then run mvn archetype:generate. Search for com.google.appengine.archetypes:skeleton-archetype and select the entry for com.google.appengine.archetypes:appengine-skeleton-archetype. Finish the wizard by specifying a unique groupId and artifactId. The rest can be left at their defaults:
C:\work\redheap\jdev12120>mvn archetype:generate
[INFO] Scanning for projects...
[INFO] 
[INFO] Using the builder org.apache.maven.lifecycle.internal.builder.singlethreaded.SingleThreadedBuilder with a thread count of 1
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] >>> maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom >>>
[INFO] 
[INFO] <<< maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom <<<
[INFO] 
[INFO] --- maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom ---
[INFO] Generating project in Interactive mode
[INFO] No archetype defined. Using maven-archetype-quickstart (org.apache.maven.archetypes:maven-archetype-quickstart:1.0)
Choose archetype:
..... [[snip long list]] .....
Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): 361: com.google.appengine.archetypes:skeleton-archetype
Choose archetype:
1: remote -> com.google.appengine.archetypes:appengine-skeleton-archetype (-)
2: remote -> com.google.appengine.archetypes:skeleton-archetype (-)
Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): : 1
Define value for property 'groupId': : com.redheap.samples
Define value for property 'artifactId': : AppEngineJaxWs
Define value for property 'version':  1.0-SNAPSHOT: : 
Define value for property 'package':  com.redheap.samples: : 
Confirm properties configuration:
groupId: com.redheap.samples
artifactId: AppEngineJaxWs
version: 1.0-SNAPSHOT
package: com.redheap.samples
 Y: : Y

In JDeveloper 12c go to Tools > Preferences and change the location of maven to use a recent version you downloaded yourself. The maven version included with JDeveloper 12.1.2.0.0 is too old for Google App Engine as that requires maven to be at least version 3.1:
Use at least version 3.1 of maven in JDeveloper 12c 

Next steps are for Oracle JDeveloper, but you can take similar steps from the command line or your favorite Java IDE. Create a new JDeveloper workspace from the maven pom files. JDeveloper will warn that the project already exists and it will be overwritten. Just click Yes even though this is a new project.
Start maven import wizard from New Gallery

Import all pom's

Point to parent directory of project as subdir is implied

Open the pom.xml at application level and update the appengine.target.version property to the latest App Engine version. Currently this is 1.9.2 but you might want to check the latest Java SDK version at https://developers.google.com/appengine/downloads.
Open the application level maven pom
Update appengine.target.version
Go to https://console.developers.google.com and create a new project. Use the same project name as you used when setting up the maven project, AppEngineJaxWs in this example. The project ID has to be globally unique across all App Engine customers and will become the first part of your URL. For example, typing redheap-jaxws will create http://redheap-jaxws.appspot.com

Open the project properties for the AppEngineJaxWs-ear project and add the src/main/application directory to the Java Source Paths.
Add src/main/application as source directory

Then open the application/META-INF/appengine-application.xml file and update the application name with the globally unique project ID (not the name) that was created in the Google developer console:
Update application name with App Engine Project ID

Next, create a Java class Person in the war project that we will be using as an argument to our webservice, just to demonstrate how to handle complex payloads:
package com.redheap.appengine;

public class Person {
    private String firstName;
    private String lastName;

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

    public String getFirstName() {
        return firstName;
    }

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

    public String getLastName() {
        return lastName;
    }
}

Next create the HelloWorld class that we will be exposing as a JAX-WS service:
package com.redheap.appengine;

public class HelloWorld {
    public String sayHello(Person person) {
        return "Hello " + person.getFirstName() + " " + person.getLastName();
    }
}

Right click the HelloWorld class in the application navigator and create a web service out of it. When running this for the first time it will ask if you want a WebLogic specific web service or JAX-WS Reference Implementation version. Be sure to check the RI one as we want to deploy to a non-weblogic environment. Other than that, accept all default values. If you are not using JDeveloper, you can also use the command line wsgen tool that ships with the JDK.


This will have added a number of annotations to the HelloWorld class. Change the @WebParam annotation for more meaningful argument name than arg0 as this name will end up in the XSD for the web service.
@WebService
public class HelloWorld {
    @WebMethod
    public String sayHello(@WebParam(name = "person") Person person) {
        return "Hello " + person.getFirstName() + " " + person.getLastName();
    }
}

Now right click HelloWorld again and generate a WSDL file.

Right click the generated wsdl file and use the Move option from the Refactor menu to relocate it to webapp directory itself (two levels up) so the file is no longer in the WEB-INF folder and accessible through the web. Repeat the same steps for the xsd file so it will also be available on the web.

Open the generated WSDL file and update the soap:address at the end of the file to the URL at your appengine instance. For example, mine is https://redheap-jaxws.appspot.com/HelloWorldService

Open the generated XSD file and add elementFormDefault="qualified" to the root element to ensure we will be using namespaces in the web service requests and responses.
Use namespace qualified elements
Now right click the XSD file and create a JAXB Content Model. Be sure to select the correct source directory for the JAXB classes in the wizard as you don't want them to end up in your test directory since those are not included in a deployment.


Google App Engine doesn't include the com.sun.xml.ws.* classes that are normally used by a JAX-WS service. So we need to create our own servlet that handles the requests and invokes the HelloWorld class. This is where the magic happens. Most of the code in this class is generic for any webservice. We implemented everything in a single class for this example, but in a real application you want to refactor the generic code to a super class that can be reused for each web service.
public class HelloWorldServlet extends HttpServlet {

    public static final String NS = "http://appengine.redheap.com/";
    public static final QName QNAME_SAY_HELLO = new QName(NS, "sayHello");
    public static final QName QNAME_OTHER = new QName(NS, "otherOperation");

    private final HelloWorld serviceImpl = new HelloWorld();

    private static final MessageFactory messageFactory;

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException,
                                                                                           IOException {
        try {
            MimeHeaders headers = getHeaders(request);
            InputStream in = request.getInputStream();
            SOAPMessage soapReq = messageFactory.createMessage(headers, in);
            SOAPMessage soapResp = handleSOAPRequest(soapReq);
            response.setStatus(HttpServletResponse.SC_OK);
            response.setContentType("text/xml;charset=\"UTF-8\"");
            OutputStream out = response.getOutputStream();
            soapResp.writeTo(out);
            out.flush();
        } catch (SOAPException e) {
            throw new IOException("exception while creating SOAP message", e);
        }
    }

    public MimeHeaders getHeaders(HttpServletRequest request) {
        MimeHeaders retval = new MimeHeaders();
        Enumeration headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement();
            String value = request.getHeader(name);
            StringTokenizer values = new StringTokenizer(value, ",");
            while (values.hasMoreTokens()) {
                retval.addHeader(name, values.nextToken().trim());
            }
        }
        return retval;
    }

    protected SOAPMessage handleSOAPRequest(SOAPMessage request) throws SOAPException {
        Iterator iter = request.getSOAPBody().getChildElements();
        Object respPojo = null;
        while (iter.hasNext()) {
            // find first Element child
            Object child = iter.next();
            if (child instanceof SOAPElement) {
                respPojo = handleSOAPRequestElement((SOAPElement) child);
                break;
            }
        }
        SOAPMessage soapResp = messageFactory.createMessage();
        SOAPBody respBody = soapResp.getSOAPBody();
        if (respPojo != null) {
            JAXB.marshal(respPojo, new SAAJResult(respBody));
        } else {
            SOAPFault fault = respBody.addFault();
            fault.setFaultString("Unknown SOAP request");
        }
        return soapResp;
    }

    protected Object handleSOAPRequestElement(SOAPElement reqElem) {
        QName reqName = reqElem.getElementQName();
        if (QNAME_SAY_HELLO.equals(reqName)) {
            return handleSayHello(JAXB.unmarshal(new DOMSource(reqElem), SayHello.class));
        } else if (QNAME_OTHER.equals(reqName)) {
            //
        }
        return null;
    }

    protected SayHelloResponse handleSayHello(SayHello request) {
        SayHelloResponse response = new SayHelloResponse();
        response.setReturn(serviceImpl.sayHello(request.getPerson()));
        return response;
    }

    static {
        try {
            messageFactory = MessageFactory.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}

Remove all entries from web.xml that were created by the web service wizard in JDeveloper and register our own servlet. The url-pattern has to match the endpoint you configured in the soap:address in the WSDL file.
<web-app version="2.5" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemalocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <servlet>
        <servlet-name>HelloWorldServlet</servlet-name>
        <servlet-class>com.redheap.appengine.HelloWorldServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>HelloWorldServlet</servlet-name>
        <url-pattern>/HelloWorldService</url-pattern>
    </servlet-mapping>
</web-app>

Finally all we need to do is add some maven goals to JDeveloper so we can select them from the build menu. To do this, go to the Tools menu and select Preferences. Then go to the Maven > Phases/Goals and add a goal to the list of Selected Phases/Goals:
Add a goal to Maven preferences

Add two goals: com.google.appengine:appengine-maven-plugin:devserver and com.google.appengine:appengine-maven-plugin:update. The first one is to start a local AppEngine server for testing and the second one is to deploy your application to the real Google App Engine.
Add Maven goals

If everything is well, you can now run your application:
  1. Right-click the pom.xml in the Application Resources > Build Files and run maven phase install from the Run Maven Goal Profile "Default" popup menu. This should compile and build your project
  2. Right-click the pom.xml in the AppEngineJaxWs-ear project and run the appengine:devserver phase from the same popup menu. This should launch the local AppEngine server and you should be able to download the WSDL at http://localhost:8080/HelloWorldService.wsdl and invoke the local webservice at http://localhost:8080/HelloWorldService
  3. Once this is fine, you can run the appengine:update phase from the AppEngineJaxWs-ear project. This installs the project at your Google AppEngine server. Be sure to run this from the command line the first time and not from JDeveloper. The reason is it will launch a web browser to authenticate yourself and you have to enter a code from the web browser into the maven build. This cannot be done when building from JDeveloper as it doesn't allow you to enter information.
You're done. Enjoy your Google AppEngine JAX-WS Soap WebService. You can also give my instance a try. The WSDL is available at https://redheap-jaxws.appspot.com/HelloWorldService.wsdl. Give it a try in SoapUI.

As always you can download the full sample application or browse the subversion repository to look at the source code. If you want to have a quick look at the solution start with the com.redheap.appengine.HelloWorldServlet servlet that does most of the important work.

1 comment:

  1. Excelent post Wilfred! thanks for sharing your knowledge!

    ReplyDelete

Comments might be held for moderation. Be sure to enable the "Notify me" checkbox so you get an email when the comment is accepted and I reply.