JAX-WS includes APIs for RESTful and SOAP-based web services, although JAX-WS seems to be used mostly for the latter. The reference implementation is Metro, which is part of the GlassFish project. Although JAX-WS technically belongs to enterprise rather than core Java, the core Java JDK (1.6 or greater) includes enough of the Metro distribution to compile and publish RESTful and SOAP-based services. JAX-RS and Restlet are state-of-the-art, high-level APIs for developing RESTful services; by contrast, the JAX-WS API for RESTful services is low-level. Nonetheless, JAX-WS support for RESTful services deserves a look, and the JAX-WS API for SOAP-based services will be the centerpiece in Chapter 4 and Chapter 5.
The JAX-WS stack reflects the view that SOAP-based services over HTTP are refinements of RESTful services. The JAX-WS API has two
main annotations. A POJO class annotated as a @WebService
delivers a SOAP-based service, whereas
a POJO class annotated as a @WebServiceProvider
usually delivers a RESTful one; however, a class annotated as
a @WebServiceProvider
can deliver
a SOAP-based service as well. Yet another revision of the adages RESTful service, adages3, introduces the JAX-WS API for
RESTful services.
In the revised adages3 service, the Adage
and Adages
classes are mostly unchanged from the adages2 version. One small change
is that the package name goes from adages2
to adages3
; another change is that the Adage
list is returned as array, which
then is serialized into XML.
The toPlain
method in the Adages
class could be dropped because the revised service deals only in application/xml
and not
in text/plain
HTTP payloads.
The AdagesProvider
class (see Example 2-19) supports the four CRUD operations against the RESTful service.
Example 2-19. The AdagesProvider
class that supports the CRUD operations
package
adages3
;
import
java.beans.XMLEncoder
;
import
java.io.ByteArrayOutputStream
;
import
java.io.ByteArrayInputStream
;
import
java.io.StringReader
;
import
java.util.ArrayList
;
import
java.util.Arrays
;
import
javax.annotation.Resource
;
import
javax.xml.transform.Source
;
import
javax.xml.transform.stream.StreamSource
;
import
javax.xml.transform.Transformer
;
import
javax.xml.transform.TransformerFactory
;
import
javax.xml.transform.stream.StreamResult
;
import
javax.xml.ws.handler.MessageContext
;
import
javax.xml.ws.WebServiceContext
;
import
javax.xml.ws.WebServiceProvider
;
import
javax.xml.ws.Provider
;
import
javax.xml.ws.BindingType
;
import
javax.xml.ws.http.HTTPBinding
;
import
javax.xml.ws.ServiceMode
;
import
javax.xml.ws.http.HTTPException
;
import
javax.xml.xpath.XPathFactory
;
import
javax.xml.xpath.XPath
;
import
javax.xml.xpath.XPathConstants
;
import
org.xml.sax.InputSource
;
@WebServiceProvider
// generic service provider
![]()
@ServiceMode
(
javax
.
xml
.
ws
.
Service
.
Mode
.
MESSAGE
)
// entire message available
![]()
@BindingType
(
HTTPBinding
.
HTTP_BINDING
)
// versus SOAP binding
![]()
public
class
AdagesProvider
implements
Provider
<
Source
>
{
@Resource
protected
WebServiceContext
wctx
;
// dependency injection
public
AdagesProvider
()
{
}
// Implement the Provider interface by defining invoke, which expects an XML
// source (perhaps null) and returns an XML source (perhaps null).
public
Source
invoke
(
Source
request
)
{
![]()
if
(
wctx
==
null
)
throw
new
RuntimeException
(
"Injection failed on wctx."
);
// Grab the message context and extract the request verb.
MessageContext
mctx
=
wctx
.
getMessageContext
();
![]()
String
httpVerb
=
(
String
)
mctx
.
get
(
MessageContext
.
HTTP_REQUEST_METHOD
);
httpVerb
=
httpVerb
.
trim
().
toUpperCase
();
// Dispatch on verb to the handler method. POST and PUT have non-null
// requests so only these two get the Source request.
if
(
httpVerb
.
equals
(
"GET"
))
return
doGet
(
mctx
);
![]()
else
if
(
httpVerb
.
equals
(
"POST"
))
return
doPost
(
request
);
else
if
(
httpVerb
.
equals
(
"PUT"
))
return
doPut
(
request
);
else
if
(
httpVerb
.
equals
(
"DELETE"
))
return
doDelete
(
mctx
);
else
throw
new
HTTPException
(
405
);
// bad verb
}
private
Source
doGet
(
MessageContext
mctx
)
{
// Parse the query string.
String
qs
=
(
String
)
mctx
.
get
(
MessageContext
.
QUERY_STRING
);
// Get all Adages.
if
(
qs
==
null
)
return
adages2Xml
();
// Get a specified Adage.
else
{
int
id
=
getId
(
qs
);
if
(
id
<
0
)
throw
new
HTTPException
(
400
);
// bad request
Adage
adage
=
Adages
.
find
(
id
);
if
(
adage
==
null
)
throw
new
HTTPException
(
404
);
// not found
return
adage2Xml
(
adage
);
}
}
private
Source
doPost
(
Source
request
)
{
if
(
request
==
null
)
throw
new
HTTPException
(
400
);
// bad request
InputSource
in
=
toInputSource
(
request
);
String
pattern
=
"//words/text()"
;
// find the Adage's "words"
String
words
=
findElement
(
pattern
,
in
);
if
(
words
==
null
)
throw
new
HTTPException
(
400
);
// bad request
Adages
.
add
(
words
);
String
msg
=
"The adage '"
+
words
+
"' has been created."
;
return
toSource
(
toXml
(
msg
));
}
private
Source
doPut
(
Source
request
)
{
if
(
request
==
null
)
throw
new
HTTPException
(
400
);
// bad request
InputSource
in
=
toInputSource
(
request
);
String
pattern
=
"//words/text()"
;
// find the Adage's "words"
String
words
=
findElement
(
pattern
,
in
);
if
(
words
==
null
)
throw
new
HTTPException
(
400
);
// bad request
// Format in XML is: <words>!<id>
String
[
]
parts
=
words
.
split
(
"!"
);
if
(
parts
[
0
].
length
()
<
1
||
parts
[
1
].
length
()
<
1
)
throw
new
HTTPException
(
400
);
// bad request
int
id
=
-
1
;
try
{
id
=
Integer
.
parseInt
(
parts
[
1
].
trim
());
}
catch
(
Exception
e
)
{
throw
new
HTTPException
(
400
);
}
// bad request
// Find and edit.
Adage
adage
=
Adages
.
find
(
id
);
if
(
adage
==
null
)
throw
new
HTTPException
(
404
);
// not found
adage
.
setWords
(
parts
[
0
]);
String
msg
=
"Adage "
+
adage
.
getId
()
+
" has been updated."
;
return
toSource
(
toXml
(
msg
));
}
private
Source
doDelete
(
MessageContext
mctx
)
{
String
qs
=
(
String
)
mctx
.
get
(
MessageContext
.
QUERY_STRING
);
// Disallow the deletion of all teams at once.
if
(
qs
==
null
)
throw
new
HTTPException
(
403
);
// illegal operation
else
{
int
id
=
getId
(
qs
);
if
(
id
<
0
)
throw
new
HTTPException
(
400
);
// bad request
Adage
adage
=
Adages
.
find
(
id
);
if
(
adage
==
null
)
throw
new
HTTPException
(
404
);
// not found
Adages
.
remove
(
adage
);
String
msg
=
"Adage "
+
id
+
" removed."
;
return
toSource
(
toXml
(
msg
));
}
}
private
int
getId
(
String
qs
)
{
int
badId
=
-
1
;
// bad ID
String
[
]
parts
=
qs
.
split
(
"="
);
if
(!
parts
[
0
].
toLowerCase
().
trim
().
equals
(
"id"
))
return
badId
;
int
goodId
=
badId
;
// for now
try
{
goodId
=
Integer
.
parseInt
(
parts
[
1
].
trim
());
}
catch
(
Exception
e
)
{
return
badId
;
}
return
goodId
;
}
private
StreamSource
adages2Xml
()
{
String
str
=
toXml
(
Adages
.
getListAsArray
());
return
toSource
(
str
);
}
private
StreamSource
adage2Xml
(
Adage
adage
)
{
String
str
=
toXml
(
adage
);
return
toSource
(
str
);
}
private
String
toXml
(
Object
obj
)
{
ByteArrayOutputStream
out
=
new
ByteArrayOutputStream
();
XMLEncoder
enc
=
new
XMLEncoder
(
out
);
enc
.
writeObject
(
obj
);
enc
.
close
();
return
out
.
toString
();
}
private
StreamSource
toSource
(
String
str
)
{
return
new
StreamSource
(
new
StringReader
(
str
));
}
private
InputSource
toInputSource
(
Source
source
)
{
InputSource
input
=
null
;
try
{
Transformer
trans
=
TransformerFactory
.
newInstance
().
newTransformer
();
ByteArrayOutputStream
bos
=
new
ByteArrayOutputStream
();
StreamResult
result
=
new
StreamResult
(
bos
);
trans
.
transform
(
source
,
result
);
input
=
new
InputSource
(
new
ByteArrayInputStream
(
bos
.
toByteArray
()));
}
catch
(
Exception
e
)
{
throw
new
HTTPException
(
500
);
}
// internal server error
return
input
;
}
private
String
findElement
(
String
expression
,
InputSource
source
)
{
XPath
xpath
=
XPathFactory
.
newInstance
().
newXPath
();
String
retval
=
null
;
try
{
retval
=
(
String
)
xpath
.
evaluate
(
expression
,
source
,
XPathConstants
.
STRING
);
}
catch
(
Exception
e
)
{
throw
new
HTTPException
(
400
);
}
// bad request
return
retval
;
}
}
Even a glance at the AdagesProvider
code looks low-level. Much of this code transforms one type
to another, for example, a Source
to an InputSource
or an Adage
to a
StreamSource
. The Source
types are sources of XML. The JAX-P (Java API for XML-Processing)
packages, used in the adages3 service, support transforms that convert a source into a result (see Figure 2-1).
For example, a generic Source
might be
transformed into a specific type such as StreamResult
. The need for such transformations in the adages3 service is
explained shortly.
First, however, it will be helpful to consider the overall structure of the AdagesProvider
.
Three annotations adorn the AdagesProvider
class:
@WebServiceProvider
(line 1) indicates that the AdagesProvider
class implements a templated Provider
interface,
in this case a Provider<Source>
interface, where Source
is a source precisely of XML. The templated
Provider
interface requires that the implementing class define the method:
public
Source
invoke
(
Source
input
)
{
/*...*/
}
This method expects a Source
of XML as an argument and returns a Source
of XML.
The AdagesProvider
class defines the invoke
method (line 2), which is the target of every HTTP request against the
adages3 service. The invoke
method implements a simple routing table. After extracting the HTTP
verb from the incoming request, using the MessageContext
map (line 5) that the runtime provides, the invoke
method calls one of four AdagesProvider
methods (line 6), each of which
returns a Source
of XML for the HTTP response body:
// mctc --> MessageContext, request --> Source
if
(
httpVerb
.
equals
(
"GET"
))
return
doGet
(
mctx
);
else
if
(
httpVerb
.
equals
(
"POST"
))
return
doPost
(
request
);
else
if
(
httpVerb
.
equals
(
"PUT"
))
return
doPut
(
request
);
else
if
(
httpVerb
.
equals
(
"DELETE"
))
return
doDelete
(
mctx
);
else
throw
new
HTTPException
(
405
);
// bad verb
This table mimics, in the method names, an HttpServlet
with its encapsulated doGet
, doPost
,
doPut
, and doDelete
methods. The difference here is that each do- method takes only one argument. The
GET and DELETE have no bodies, which means that the incoming Source
is empty. In any case, the
doGet
and doDelete
methods need only information in the query string, which is stored in the
HTTP headers; hence, doGet
and doDelete
are passed the MessageContext
as the argument. By contrast,
the doPost
and doPut
methods require information in the HTTP request body, a nonempty Source
;
these methods therefore are passed the request Source
as their single argument.
@ServiceMode
annotation (line 2) has two possible values: MESSAGE
(the entire request message) or
PAYLOAD
(the body, if any, of the request message). The adages3 service needs access to both
the HTTP headers and, for POST and PUT requests, the HTTP body; hence, the service mode is
MESSAGE
.
@BindingType
(line 3) refers to the type of payload in an HTTP message. The default type is
SOAP, which means that the body of an HTTP message (for instance, a POST request or any response)
is a SOAP document. The AdagesProvider
specifies an HTTP binding, which means that the
HTTP payload is to be arbitrary XML, not exclusively the SOAP variant of XML. Even with this
binding type, however, a SOAP document could be a payload because SOAP still counts as XML.
This overview should help in the more detailed analysis that follows. In the adages3 service,
the doGet
method needs to handle two cases:
XMLEncoder
,
is returned.
If there is a query string, it should have a key/value pair such as:
id
=
4
where 4 is then interpreted as the id
of the single message to be returned. Here, for quick review,
is the doGet
method:
private
Source
doGet
(
MessageContext
mctx
)
{
// Parse the query string.
String
qs
=
(
String
)
mctx
.
get
(
MessageContext
.
QUERY_STRING
);
if
(
qs
==
null
)
return
adages2Xml
();
// all adages
![]()
else
{
// one adage
int
id
=
getId
(
qs
);
if
(
id
<
0
)
throw
new
HTTPException
(
400
);
// bad request
Adage
adage
=
Adages
.
find
(
id
);
if
(
adage
==
null
)
throw
new
HTTPException
(
404
);
// not found
return
adage2Xml
(
adage
);
}
}
Utility methods such as adages2Xml
(line 1) handle the transformation of Adage
objects
into XML documents (text), which in turn are transformed into StreamSource
instances sent back
to the client.
The doPost
method is:
private
Source
doPost
(
Source
request
)
{
if
(
request
==
null
)
throw
new
HTTPException
(
400
);
// bad request
InputSource
in
=
toInputSource
(
request
);
![]()
String
pattern
=
"//words/text()"
;
// find the Adage's "words"
![]()
String
words
=
findElement
(
pattern
,
in
);
if
(
words
==
null
)
throw
new
HTTPException
(
400
);
// bad request
Adages
.
add
(
words
);
String
msg
=
"The adage '"
+
words
+
"' has been created."
;
return
toSource
(
toXml
(
msg
));
}
This method relies on utility methods, in particular on the tricky toInputSource
method (line 1) that
transforms a Source
request, which is likely but not necessarily a StreamSource
,
into an InputSource
. The reason is that, for convenience, the doPost
method uses
an XPath
instance to search the incoming XML document for the words
in the Adage
to be created. For example, the XML document might look like this in a POST request:
<
ns1:
foo
xmlns:
ns1
=
'
http:
//sample.org'>
<
words
>
This
is
the
way
the
world
ends
.</
words
>
</
ns1:
foo
>
An XPath
search requires a pattern, in this example (line 2):
//words/text()
The two opening slashes mean anywhere in the document and the specific search term is
the literal words
. The text()
at the end signals XPath
to return the text node in the
XML document that contains the new adage; in this case, the phrase:
This
is
the
way
the
world
ends
.
The search is flexible in that the XML tag words could be anywhere in the document, in
this example nested inside the root element named ns1:foo. Now let me get back to the
point about needing to transform a Source
into an InputSource
. The XPath
method
evaluate
searches an XML document for a pattern such as //words but requires, as
a second argument, an InputSource
; hence, the transformation of the incoming but
generic Source
to an InputSource
sets up the XPath
search. There are other ways in
which the XPath
search might be supported but any of these would require a
transformation of some kind.
The doPut
method in the AdagesProvider
class is
similar in structure to the doPost
method because, of course, creating a new
Adage
(POST) and updating an existing one (PUT) are similar operations. However, the
doPut
implementation allows only the words
of the Adage
to be changed; the id
property, which the Adages
class manages, cannot be changed through a PUT operation.
The JAX-WS @WebServiceProvider
is a low-level, XML-centric API. Java is well known for providing
options, and this API is among the Java options for delivering REST-style services. Chapter 7 introduces
the client-side API, based on the Dispatch
interface, for RESTful services implemented with
@WebServiceProvider
.