Posted by kohlert on January 17, 2006 at 12:11 PM | Comments (12)
The EA3 version of JAX-WS or the JAX-WS released in JWSDP 2.0, supports the publishing and use RESTful Web Services. Here is an example that shows how to publish a RESTful Web Service using JAX-WS.
Publishing a RESTful Web Service with JAX-WS starts by creating an implementation of the javax.xml.ws.Provider<T> interface. The Provider interface is the dynamic alternative to a standard endpoint implementation class. It is analogous to the javax.xml.ws.Dispatch interface used on the client. You will notice that Provider<T> is a generic class. It can support Provider<javax.xml.transform.Source> and
Provider<javax.xml.soap.SOAPMessage> with the SOAP/HTTP binding, or Provider<javax.activation.DataSource> and
Provider<javax.xml.transform.Source> with XML/HTTP binding. When creating an
implementation of Provider you choose which form the requests and response messages will be processed by your implementation.
The example Web Service we are going to create is very simple, it will add two numbers and return the result ad we will use Provider<Source> for our Provider implementation and we will use the XML/HTTP binding.
The first step in writing a the AddNumbers Provider implementation is to declare the AddNumbers class.
public class AddNumbersImpl implements Provider {
}
The next step is to declare a @Resource annotation that will be used by the JAX-WS runtime to inject a WebServiceContext into our AddNumbersImpl instance.
public class AddNumbersImpl implements Provider {
@Resource
protected WebServiceContext wsContext;
}
The next step is to implement the T Provider.invoke(T request) method. To do this we will first declared the following method declaration which has a simple try-catch block to handle exceptions. Notice that this method takes a Source object as the request and returns a Source object as the response. This corresponds to the type of Provider that we created.
public class AddNumbersImpl implements Provider {
@Resource
protected WebServiceContext wsContext;
public Source invoke(Source request) {
try {
} catch(Exception e) {
e.printStackTrace();
throw new HTTPException(500);
}
}
}
For this example, the AddNumbers web service will accept requests by extracting the numbers to be added from either the the URL path or from an HTTP query. The query string and path string can be retrieved from the MessageContext which is retrieved from the WebServiceContext wsContext that will be injected into our AddNumbers object. The following code can be used to retrieve the PATH_INFO from the URL and check to see if it is in the proper format.
The createResultSource(String str) method simply creates a Source object from a properly formatted MessageContext.PATH_INFO string, by extracting the two numbers to be added from the path and adding them together and calling the createResultSource(int sum)method. The source for these two methods are:
private Source createResultSource(String str) {
StringTokenizer st = new StringTokenizer(str, "=&/");
String token = st.nextToken();
int number1 = Integer.parseInt(st.nextToken());
st.nextToken();
int number2 = Integer.parseInt(st.nextToken());
int sum = number1+number2;
return createResultSource(sum);
}
private Source createResultSource(int sum) {
String body =
"<ns:addNumbersResponse xmlns:ns=\"http://java.duke.org\"><ns:return>"
+sum
+"</ns:return></ns:addNumbersResponse>";
Source source = new StreamSource(
new ByteArrayInputStream(body.getBytes()));
return source;
}
So our example invoke method looks like the following.
public Source invoke(Source source) {
try {
MessageContext mc = wsContext.getMessageContext();
// check for a PATH_INFO request
String path = (String)mc.get(MessageContext.PATH_INFO);
if (path != null && path.contains("/num1") &&
path.contains("/num2")) {
return createResultSource(path);
}
throw new HTTPException(404);
} catch(Exception e) {
e.printStackTrace();
throw new HTTPException(500);
}
}
You will notice that if the proper MessageContext.PATH_INFO is not found, the example throws a HTTPException(404). Instead of throwing this exception, our example endpoint can instead check the query string for the method arguments. Like the MessageContext.PATH_INFO, the query string can be retrieved from the MessageContext with the following code.
Given this we can either pass the query string to the
createResultSource(String str) method which will parse the parameters from the query string or we
can use the standard ServletRequest object to extract the query arguments if we are deploying to a servlet container as we are for this example. If we were to deploy a Java SE based endpoint we would have to use the MessageContext.QUERY_STRING.
The following code extracts the num1 and num2 query values, adds them together and invokes the createResultSource(int sum) method to create the Source object that will be returned from the invoke method.
ServletRequest req = (ServletRequest)mc.get(MessageContext.SERVLET_REQUEST);
int num1 = Integer.parseInt(req.getParameter("num1"));
int num2 = Integer.parseInt(req.getParameter("num2"));
return createResultSource(num1+num2);
So far, our AddNumbers class looks like the following.
public class AddNumbersImpl implements Provider {
@Resource
protected WebServiceContext wsContext;
public Source invoke(Source source) {
try {
MessageContext mc = wsContext.getMessageContext();
// check for a PATH_INFO request
String path = (String)mc.get(MessageContext.PATH_INFO);
if (path != null && path.contains("/num1") &&
path.contains("/num2")) {
return createResultSource(path);
}
String query = (String)mc.get(MessageContext.QUERY_STRING);
System.out.println("Query String = "+query);
ServletRequest req = (ServletRequest)mc.get(MessageContext.SERVLET_REQUEST);
int num1 = Integer.parseInt(req.getParameter("num1"));
int num2 = Integer.parseInt(req.getParameter("num2"));
return createResultSource(num1+num2);
} catch(Exception e) {
e.printStackTrace();
throw new HTTPException(500);
}
}
private Source createResultSource(String str) {
StringTokenizer st = new StringTokenizer(str, "=&/");
String token = st.nextToken();
int number1 = Integer.parseInt(st.nextToken());
st.nextToken();
int number2 = Integer.parseInt(st.nextToken());
int sum = number1+number2;
return createResultSource(sum);
}
private Source createResultSource(int sum) {
String body =
"<ns:addNumbersResponse xmlns:ns=\"http://java.duke.org\"><ns:return>"
+sum
+"</ns:return></ns:addNumbersResponse>";
Source source = new StreamSource(
new ByteArrayInputStream(body.getBytes()));
return source;
}
}
To finish off the AddNumbers, we need to declare some annotions on the class that instruct the JAX-WS runtime how to use this class. The @WebServiceProvider annotation specifies that this class is a Provider based endpoint rather than a service endpoint implementation class that would be annotated with @WebService. The final annotation we add is @BindingType(value=HTTPBinding.HTTP_BINDING). This annotation specifies that AddNumbers endpoint should be published using the HTTPBinding.HTTP_BINDING as opposed to a
SOAPBinding.SOAP11HTTP_BINDING or a SOAPBinding.SOAP12HTTP_BINDING binding. The resulting class looks like the following.
@WebServiceProvider
@BindingType(value=HTTPBinding.HTTP_BINDING)
public class AddNumbersImpl implements Provider {
@Resource
protected WebServiceContext wsContext;
public Source invoke(Source source) {
try {
MessageContext mc = wsContext.getMessageContext();
// check for a PATH_INFO request
String path = (String)mc.get(MessageContext.PATH_INFO);
if (path != null && path.contains("/num1") &&
path.contains("/num2")) {
return createResultSource(path);
}
String query = (String)mc.get(MessageContext.QUERY_STRING);
System.out.println("Query String = "+query);
ServletRequest req = (ServletRequest)mc.get(MessageContext.SERVLET_REQUEST);
int num1 = Integer.parseInt(req.getParameter("num1"));
int num2 = Integer.parseInt(req.getParameter("num2"));
return createResultSource(num1+num2);
} catch(Exception e) {
e.printStackTrace();
throw new HTTPException(500);
}
}
private Source createResultSource(String str) {
StringTokenizer st = new StringTokenizer(str, "=&/");
String token = st.nextToken();
int number1 = Integer.parseInt(st.nextToken());
st.nextToken();
int number2 = Integer.parseInt(st.nextToken());
int sum = number1+number2;
return createResultSource(sum);
}
private Source createResultSource(int sum) {
String body =
"<ns:addNumbersResponse xmlns:ns=\"http://java.duke.org\"><ns:return>"
+sum
+"</ns:return></ns:addNumbersResponse>";
Source source = new StreamSource(
new ByteArrayInputStream(body.getBytes()));
return source;
}
}
To deploy our endpoint on a servlet container running with the JAX-WS RI we need to create a WAR file. For this example, we will name the WAR file jaxws-restful.war. The WAR file needs a very simple web.xml file to configure the JAX-WS RI servlet. The following will work.
You will notice in the web.xml and sun-jaxws.xml the presence of a
<url-pattern>/addnumbers/*</url-pattern> element
and a url-pattern="/addnumbers/*" attrbute,
these are specified so we can match any extra arguments that might be in the path (e.g. .../num1/10/num2/20).
You will also notice from the sun-jaxws.xml deployment descriptor that a wsdl file is specified. This is currently necessary to tell the JAX-WS RI not to try to generate a WSDL for this endpoint. The contents of AddNumbers.wsdl is simply the following.
Once the AddNumbers endpoint has been deployed, you can test it by entering the following URLs into a browser (slight modifications maybe necessary depending on your own configuration).
http://localhost:8080/jaxws-restful/addnumbers/num1/10/num2/20
Should return the following:
Comments are listed in date ascending order (oldest first) | Post Comment
Looks petty complex. What are the advantages of using JAX-WS for a RESTful web service instead of just writing a simple Servlet or JSP that reads and writes XML?
Posted by: snoopdave on January 17, 2006 at 12:34 PM
Hi Doug,
This is quite interesting. There's something that's puzzling me though: where do the
<ns:addNumbersResponse xmlns:ns="http://duke.org"> and <ns:return>
elements comes from?
Is there some hidden magic that creates these elements or is it simply missing from the createResultSource(int sum) code extract?
regards,
Posted by: dfuchs on January 18, 2006 at 06:20 AM
Perhaps this is a naive question, but I'm having issues processing the AddNumbersImpl.java annotations. How should we be compiling that? I keep getting "error: A web service endpoint could not be found" when using wsgen or apt.
Posted by: osmedd on January 18, 2006 at 11:56 AM
Granted, there are many ways to provide such a web service as the one in this sample and doing so with a servlet or JSP is of course possible. The advantage of using this approach in JAX-WS is that you get the ability to use JAX-WS handlers if desired. Again, this blog does is not showing why you would want to do this, rather, it's purpose is to show it is possible and to allow the developer to choose how to implement it.
Posted by: kohlert on January 18, 2006 at 01:17 PM
dfuchs,
Thank you for catching this, I forgot to escape the *< symbols. I have corrected the blog to show the proper method and here it is again.
private Source createResultSource(int sum) {
String body =
"<ns:addNumbersResponse xmlns:ns=\"http://duke.org\"><ns:return>"
+sum
+"</ns:return></ns:addNumbersResponse>";
Source source = new StreamSource(
new ByteArrayInputStream(body.getBytes()));
return source;
}
Posted by: kohlert on January 18, 2006 at 01:30 PM
osmedd,
This maybe a bug, I need to investigate further. However, since this sample uses the Provider approach, there is no need to use apt or wsgen for this example, you should be able to just compile it with javac as JAX-WS does not need to generate any additional artifacts to make Provider based endpoints work. I will look into the issue of apt and wsgen giving an error on Provider based endpoints.
Thank you for bringing this to my attention.
Posted by: kohlert on January 18, 2006 at 01:34 PM
I am trying to use the similar approach, but i need to read from the source instead of reading from PATH_INFO or QUERY_STRING. It will be great if some one can post a simple client and the code to demonstrate the use of source.
Posted by: kishoreg on March 26, 2006 at 09:52 AM
snoopdave, the advantage is that you don't have to write a servlet that can read & write XML :) Seriously though, this is actually quite a bit simpler than writing a custom service. Using this article as a starting point, I create a RESTful service using JAX-WS is about 10 minutes.
The thing that sets JAX-WS apart from using a servlet: you can run your service outside of a servlet container. This is very important when it comes to testing your service. I'm very impressed with the JAX-WS stack overall. Nice work guys and thanks!
Ryan-
Posted by: damnhandy on October 22, 2006 at 07:40 AM
keep getting this error for this example:
I am using tomcat 6 java 6, jax-ws 2.1
WSSERVLET11: failed to parse runtime descriptor: javax.xml.ws.WebServiceException: Not a primary WSDL=jndi:/localhost/WsWrapper/WEB-INF/wsdl/WsWrapper.wsdl since it doesn't have Service
Posted by: jxarms3 on March 27, 2007 at 11:34 AM
Jxarms3,
Did you write the application yourself or did you run the restul sample that is part of JAX-WS? This blog was taken from the sample, so if you wrote it yourself, please try to run the sample provided.
Thanks
Posted by: kohlert on April 06, 2007 at 01:51 PM
It will be great if some one can post a simple client and the code to demonstrate the use of source. 記事
Posted by: winrelocation on October 30, 2007 at 01:34 AM
I am trying to deploy an even simpler version of your sample code to JBoss-5.0 Beta 3, but it keeps giving me the 'ClassNotFoundException' on com.sun.xml.ws.transport.http.servlet.WSServletContextListener. I copied the jar files from Metro project to the classpath, and I know that class is in those jar files. Anybody has any idea about what I need to do to make it work?
Posted by: jiehuanli on February 06, 2008 at 01:04 PM