This getting started guide will attempt to walk through a mock example of Web service development integrated with Enunciate, without trying to hide the complexities of deployment descriptors, dependencies, configuration, and packaging. Of course, some attempt to be concise needs to be made, but at least you'll get the idea of the full development effort. And you'll see more than just "Hello, World".
We'll be writing a mock social networking app. For this tutorial, the name of our social networking app is IfYouWannaBeCool.com.
- Step 1: Write the Code
- Step 2: Create the Project Descriptors
- Step 3: Invoke Enunciate
- Step 4: Take a Look
- Step 5 (Optional): Iterate
The source code for this application can be obtained in one of two ways:
The "wannabecool" sample is available as a public repository at stoicflame/enunciate-sample
.
Enunciate provides a Maven archetype to seed a simple web service project. The project uses the basic modules and includes working sample service and model code. Here's how to invoke it:
mvn archetype:generate \
-DarchetypeGroupId=com.webcohesion.enunciate \
-DarchetypeArtifactId=enunciate-simple-archetype \
-DarchetypeVersion=2.8.0 \
-DgroupId=com.ifyouwannabecool \
-DartifactId=enunciate-sample
We'll start by defining the domain of our app. We'll define a Persona
with an id, an alias, an email address, a name, and a picture. The Name
is a complex structure, made up of a given name and a surname. We'll also define a Link
between two personas and a SocialGroup
consisting of an id, a group leader, the group members, and whether the group is exclusive.
Next, we'll define the services available for our domain data. The PersonaService
will define the operations available on a persona. This includes operations for reading a persona, storing a persona, and deleting a persona. The
SocialGroupService
will
carry the operations that deal with linking and grouping personas. These operations include creating a link between two personas, creating a social group,
adding someone to a social group, and reading the social groups of a given persona.
We also define the possible exceptions that can get thrown, including a PermissionDeniedException
when trying to create a link between two people, and an ExclusiveGroupException
when trying to add a persona to an exclusive group.
After having defined our service interfaces, we create our implementation classes, PersonaServiceImpl
and SocialGroupServiceImpl
.
In order for our code to be identified as a Web service API, we need to apply some metadata in the form of Java annotations. We'll start by defining a resource API using JAX-RS annotations. Our objects will be converted to XML using JAXB.
A JAX-RS endpoint is identified by mapping it's implementation to an URI path using the @javax.ws.rs.Path
annotation. We'll apply the PersonaServiceImpl
endpoint implementation to the "/persona" path and the SocialGroupServiceImpl
to the "/group" path.
Then, using JAX-RS, we map each method to an HTTP operation and, optionally, to a subpath. We decide to apply method-level metadata on the interface that defines the method. (Note that using interfaces isn't strictly necessary, but it may be convenient to do so if and/or when we apply some aspects or dependency injection.)
So for the PersonaService
, we will use an HTTP GET
to access the readPersona
method and will pass in the persona id as a parameter on the path. We will
use an HTTP POST
operation to store a new persona, and an HTTP DELETE
method to delete a person.
Then, for the SocialGroupService
, we will use an HTTP GET
to access the readGroup
method and will pass in the group id as a parameter on the path.
To store a new group, we will use the HTTP POST
method and pass in the parameters as HTTP query parameters. To add a persona to a group, we'll use a POST
to the group path and pass in the persona id as a query parameter.
Because we're returning objects from a few of our REST API calls, we need to also apply the @XmlRootElement
annotation so that JAXB will know how to write
out the root of our XML tree.
So, we've got our domain defined along with some services that operate on the domain.
Now we need to create the configuration files that describe the project. This includes build files and deployment descriptors. For this sample project, we'll use either Maven or Ant to build a war file that can be deployed to a standard J2EE servlet container.
In order to get a running Web service API, you need to choose a JAX-RS implementation. This sample uses Jersey. Other perfectly good JAX-RS implementations include Resteasy and CXF.
In order to describe our application to the JAX-RS implementation, we define an implementation of javax.ws.rs.core.Application
. The particular
implementation selected for this sample application provides an abstract class, org.glassfish.jersey.server.ResourceConfig
that makes it easy to
define our application. To that end, we create a class called App
that tells Jersey which packages to recursively scan to find our JAX-RS resource API.
For Maven, you need a pom.xml
file. The packaging
of the project will be war
and it uses the enunciate-maven-plugin
to integrate Enunciate into the build. For development convenience, we also use
the cargo-maven2-plugin
to make it easy to deploy our application.
For Ant, you need a build.xml
file. For convenience, this
project uses Maven Ant Tasks to manage the project dependencies (since we already have a pom.xml file), but other
dependency management tools (e.g. Ivy) would work just as well. (Note: the Ant build for this project only generates the
Enunciate documentation; the compile and package work is left as an exercise for the reader.)
We create a web.xml
file that tells the J2EE
container how to deploy our application. This particular example, configures Jersey as a servlet filter so that requests for the Enunciate-generated
documentation will be forwarded to the container and resolved.
Once we have our source code and project descriptors, the project tree looks like the following (you can see and browse it here):
pom.xml
build.xml
src
└── main
├── java
└── com
└── ifyouwannabecool
├── api
│ ├── ExclusiveGroupException.java
│ ├── PermissionDeniedException.java
│ ├── PersonaService.java
│ └── SocialGroupService.java
├── App.java
├── domain
│ ├── link
│ │ ├── Link.java
│ │ └── SocialGroup.java
│ └── persona
│ ├── Name.java
│ └── Persona.java
└── impl
├── PersonaServiceImpl.java
└── SocialGroupServiceImpl.java
Next, simply invoke Maven:
user@localhost>mvn package
Or Ant:
user@localhost>ant
If you're using Maven, the result of the invocation will be a war
file that you can deploy to your favorite J2EE application container. Alternatively,
you can invoke Cargo:
user@localhost>mvn cargo:run
If invoking cargo, you'll see the application running at [http://localhost:8080/enunciate-sample].
The first thing you'll notice is a nice-looking web page with a generic title divided into two sections. The first section describes the Resource API, and it includes the two services we just wrote. The second section is the data section describing our data types. You'll notice links the XML-Schema file for our data types, a WADL file that describes the resource API, and a link to the Swagger UI for the API.
As you continue to poke around, you'll notice that the documentation is quite sparse (although some information can be gleaned from the names of the methods and arguments).
The "Files and Libraries" page links to client-side binaries and source code that can be used to access your API. These client-side libraries were generated by Enunciate and packaged up (along with everything else) in the war. The client-side code is clean, intuitive, and powerful, handling all the complexities of the API.
Even with no extra options or decorations, Enunciate does a pretty good job of documenting your API. But there's so much more you can do with only a minimal amount of effort! Let's give our classes some extra love, then we'll talk about what we did.
src
|
|--enunciate.xml
|--LICENSE.txt
|--com
|
|--ifyouwannabecool
|
|--api
| |
| |--ExclusiveGroupException.java
| |--SocialGroupService.java
| |--PermissionDeniedException.java
| |--PersonaService.java
| |--package-info.java
|--domain
| |
| |--link
| | |
| | |--Link.java
| | |--SocialGroup.java
| | |--package-info.java
| |
| |--persona
| |
| |--Name.java
| |--Persona.java
| |--package-info.java
|
|--impl
|
|--SocialGroupServiceImpl.java
|--PersonaServiceImpl.java
|--package-info.java
Adding a SOAP API is as simple as applying the JAX-WS @WebService
annotation to our service interfaces and to our service implementations. (Note that per the
JAX-WS specification, the implementation must reference the interface using the endpointInterface
method on the annotation.) We also apply the @WebFault
annotation to our exceptions so they'll be translated to the client correctly.
It is considered a best practice to namespace-qualify your domain API. We did this with the use of the package-info.java files for the link API and the persona API. Namespace-qualifying your domain API ensures maximum compatibility as the use of the default namespace is confusing to implementation vendors. It also provides a tool to help with versioning your API.
You'll notice we added a package-info.java
file to the com.ifyouwannabecool.api
package. This is where we added the introductory (i.e. "splash")
documentation for our API. We want this documentation to show up on our index page for our documentation, so we specify this package as the splash
packge in our enunciate.xml
configuration file. For more information, check out the user guide.
We've added javadocs to everything, including the endpoints, their methods and parameters, and the data types. This documentation will show up in our generated documentation. You're free to use HTML tags as you want; they'll be applied in the resulting HTML. Javadoc block tags are recognized, but currently there is no support for javadoc inline tags (they'll show up unparsed in the documentation).
We can specify a title and copyright to the generated documentation in the enunciate.xml
configuration file.
We want to be able to add a downloadable file to the documentation. In this case, we add the license file that governs the use of the API. This extra download
is specified in the enunciate.xml
configuration file.
We specify our hostname ("www.ifyouwannabecool.com") and the context at which the app will be deployed ("api") in the enunciate.xml
configuration file. There
are two advantages to this. (1) The generated WSDL will have an absolute reference to our SOAP endpoints, making the formal XML contract complete. (2) Consumers
of our client code won't have to specify the URL of our endpoints if they don't want to.
Again, done in the enunciate.xml
configuration file. Why would we want to do this? For aesthetics; the default "ns0", "ns1",
etc. prefixes that are automatically assigned are kind of ugly. It also gives a nice name to our wsdl and schema files.
Done (where else?) in the enunciate.xml
configuration file. This will distinguish the generated client classes from the original server-side classes. (Makes
it easier to test, clearer that you're dealing with client-side classes, etc.)
And there are still a ton of other configuration options available to you as the API developer (which we won't go into here). These include options to specify your own CSS for the documentation, or even your own freemarker template if you don't like the structure, etc.
When we're done polishing things up, we run our build again and deploy the war file as we did in steps 3 and 4.
Take a look now at the generated documentation and you'll notice all the new enhancements!