This section ports the various REST-style versions of the predictions web service to a SOAP-based version. The new version is predictionsSOAP, whose structure can be summarized as follows:
portType
section, that a client request may result in a
fault message rather than a standard response. The architecture of the predictionsSOAP service is
sketched in Figure 5-5.
The predictionsSOAP service mirrors, in its structure, the SOAP-based version of Amazon’s E-Commerce service. In each case, a client-side handler modifies an outgoing SOAP message by inserting a security credential; a service-side handler then verifies the credential before dealing with the request itself.
The PredictionsSOAP
class (see Example 5-3), the main class for the
predictionsSOAP service, has five service operations: getAll,
getOne, create, edit, and delete.
Example 5-3. The PredictionsSOAP
class with two methods that throw SOAP faults
package
predictions
;
import
javax.annotation.Resource
;
import
javax.jws.WebService
;
import
javax.jws.WebMethod
;
import
javax.jws.HandlerChain
;
import
javax.xml.ws.WebServiceContext
;
import
javax.xml.ws.handler.MessageContext
;
import
java.util.List
;
import
javax.servlet.ServletContext
;
@WebService
@HandlerChain
(
file
=
"../../../WEB-INF/serviceHandler.xml"
)
public
class
PredictionsSOAP
{
@Resource
private
WebServiceContext
wsCtx
;
private
ServletContext
sCtx
;
private
static
final
Predictions
predictions
=
new
Predictions
();
private
static
final
int
maxLength
=
16
;
@WebMethod
public
List
<
Prediction
>
getAll
()
{
init
();
return
predictions
.
getPredictions
();
}
@WebMethod
public
Prediction
getOne
(
int
id
)
{
init
();
return
predictions
.
getPrediction
(
id
);
}
@WebMethod
public
String
create
(
String
who
,
String
what
)
throws
VerbosityException
{
![]()
int
count
=
wordCount
(
what
);
if
(
count
>
maxLength
)
throw
new
VerbosityException
(
count
+
" is too verbose!"
,
"Max words: "
+
maxLength
);
init
();
Prediction
p
=
new
Prediction
();
p
.
setWho
(
who
);
p
.
setWhat
(
what
);
int
id
=
predictions
.
addPrediction
(
p
);
String
msg
=
"Prediction "
+
id
+
" created."
;
return
msg
;
}
@WebMethod
public
String
edit
(
int
id
,
String
who
,
String
what
)
throws
VerbosityException
{
![]()
int
count
=
wordCount
(
what
);
if
(
count
>
maxLength
)
throw
new
VerbosityException
(
count
+
" is too verbose!"
,
"Max words: "
+
maxLength
);
init
();
String
msg
=
"Prediction "
+
id
+
" not found."
;
Prediction
p
=
predictions
.
getPrediction
(
id
);
if
(
p
!=
null
)
{
if
(
who
!=
null
)
p
.
setWho
(
who
);
if
(
what
!=
null
)
p
.
setWhat
(
what
);
msg
=
"Prediction "
+
id
+
" updated."
;
}
return
msg
;
}
@WebMethod
public
String
delete
(
int
id
)
{
init
();
String
msg
=
"Prediction "
+
id
+
" not found."
;
Prediction
p
=
predictions
.
getPrediction
(
id
);
if
(
p
!=
null
)
{
predictions
.
getMap
().
remove
(
id
);
msg
=
"Prediction "
+
id
+
" removed."
;
}
return
msg
;
}
private
void
init
()
{
if
(
wsCtx
==
null
)
throw
new
RuntimeException
(
"DI failed on wsCtx!"
);
if
(
sCtx
==
null
)
{
// ServletContext not yet set?
MessageContext
mCtx
=
wsCtx
.
getMessageContext
();
sCtx
=
(
ServletContext
)
mCtx
.
get
(
MessageContext
.
SERVLET_CONTEXT
);
predictions
.
setServletContext
(
sCtx
);
}
}
private
int
wordCount
(
String
words
)
{
if
(
words
==
null
)
return
-
1
;
return
words
.
trim
().
split
(
"\\s+"
).
length
;
}
}
Two of the implementing methods, create
and edit (lines 1 and 2), throw an exception named VerbosityException
if the creation of a new
Prediction
or the editing of an existing one results in a candidate prediction
that exceeds the maximum length, currently set to 16 words. Each of the methods
create and edit is annotated as a @Web
Method
and each throws
a VerbosityException
, which becomes a fault at the SOAP level. As a result,
the service’s WSDL now contains an extra message in the portType
section for
the create and edit operations. Here is a WSDL segment that includes
the portion for create and, for contrast, delete:
<
portType
name
=
"PredictionsSOAP"
>
<
operation
name
=
"delete"
>
<
input
wsam:
Action
=
"http://predictions/PredictionsSOAP/deleteRequest"
![]()
message
=
"tns:delete"
/>
<
output
wsam:
Action
=
"http://predictions/PredictionsSOAP/deleteResponse"
message
=
"tns:deleteResponse"
/>
</
operation
>
<
operation
name
=
"create"
>
<
input
wsam:
Action
=
"http://predictions/PredictionsSOAP/createRequest"
message
=
"tns:create"
/>
<
output
wsam:
Action
=
"http://predictions/PredictionsSOAP/createResponse"
message
=
"tns:createResponse"
/>
<
fault
message
=
"tns:VerbosityException"
![]()
name
=
"VerbosityException"
wsam:
Action
=
"http://predictions/PredictionsSOAP/create/Fault/VerbosityException"
/>
</
operation
>
...
The delete operation has the usual input
and output
messages (line 1), whereas the
create operation now has, in addition to the usual two, a fault
message as well (line 2); the fault message, like all of the other messages, is defined in the XML Schema.
A VerbosityException
is thrown at the application rather than at the handler level. Accordingly,
the underlying SOAP libraries handle the details of converting a Java Exception
into a SOAP fault
message. The VerbosityException
class is standard Java:
package
predictions
;
public
class
VerbosityException
extends
Exception
{
private
String
details
;
public
VerbosityException
(
String
reason
,
String
details
)
{
super
(
reason
);
![]()
this
.
details
=
details
;
}
public
String
getFaultInfo
()
{
return
this
.
details
;
}
![]()
}
A VerbosityException
has a reason
(line 1) to explain why the fault occurred together with details
(line 2)
that provide additional information.
Both the reason
and the details
become part of the SOAP fault message.
Generating a VerbosityException
is standard Java—a throw
clause is used to generate an exception.
The bodies of the create
and edit
methods begin in the same way—with a check of whether the submitted Prediction
(the parameter name is what
)
is too long:
int
count
=
wordCount
(
what
);
if
(
count
>
maxLength
)
throw
new
VerbosityException
(
count
+
" is too verbose!"
,
![]()
"Max words: "
+
maxLength
);
If a candidate Prediction
exceeds the maximum length,
a VerbosityException
is thrown (line 1) with the regular Java syntax.
Generating a SOAP fault at the application level requires just two conditions:
@WebMethod
) throws a customized Exception
, in this
case a VerbosityException
.
Exception
should invoke the superclass
constructor with the reason for the fault (line 1 in the full listing) and also should implement
the method getFaultInfo
(line 2 in the full listing), which can provide additional detail about the fault.
For the fault message returned from the PredictionsSOAP service when a submitted prediction is 18 words in length, see Example 5-4.
Example 5-4. A fault generated from a verbose prediction
<
S:
Envelope
xmlns:
S
=
"http://schemas.xmlsoap.org/soap/envelope/"
>
<
S:
Header
/>
<
S:
Body
>
<
S:
Fault
xmlns:
ns4
=
"http://www.w3.org/2003/05/soap-envelope"
>
<
faultcode
>
S:
Server
</
faultcode
>
<
faultstring
>
18
is
too
verbose
!</
faultstring
>
<
detail
>
<
ns2:
VerbosityException
xmlns:
ns2
=
"http://predictions/"
>
<
faultInfo
>
Max
words:
16
</
faultInfo
>
<
message
>
18
is
too
verbose
!</
message
>
</
ns2:
VerbosityException
>
</
detail
>
</
S:
Fault
>
</
S:
Body
>
</
S:
Envelope
>
The PredictionsSOAP
class has backend classes in support, in particular Prediction
(see Example 5-5)
and Predictions
(see Example 5-6). Neither the PredictionsSOAP
class nor any of the backend classes does any
explicit XML processing, of course,
because the underlying
SOAP libraries handle the serialization and deserialization automatically.
Example 5-5. The Prediction
supporting class
package
predictions
;
import
java.io.Serializable
;
public
class
Prediction
implements
Serializable
,
Comparable
<
Prediction
>
{
private
String
who
;
// person
private
String
what
;
// his/her prediction
private
int
id
;
// identifier used as lookup key
public
Prediction
()
{
}
public
void
setWho
(
String
who
)
{
this
.
who
=
who
;
}
public
String
getWho
()
{
return
this
.
who
;
}
public
void
setWhat
(
String
what
)
{
this
.
what
=
what
;
}
public
String
getWhat
()
{
return
this
.
what
;
}
public
void
setId
(
int
id
)
{
this
.
id
=
id
;
}
public
int
getId
()
{
return
this
.
id
;
}
public
int
compareTo
(
Prediction
other
)
{
return
this
.
id
-
other
.
id
;
}
}
The Prediction
class implements Comparable
and, therefore, defines the compareTo
method so that a client against the predictionsSOAP service can get a sorted list of Predictions
on a getAll
request. Otherwise, the Prediction
class is a POJO class
with three properties: id
, which identifies a Prediction
; who
, which names the
author of the Prediction
; and what
, which consists of the actual words in the
Prediction
.
Example 5-6. The Predictions
supporting class
package
predictions
;
import
java.io.IOException
;
import
java.io.InputStream
;
import
java.io.InputStreamReader
;
import
java.io.BufferedReader
;
import
java.io.ByteArrayOutputStream
;
import
java.util.Arrays
;
import
java.util.List
;
import
java.util.ArrayList
;
import
java.util.concurrent.ConcurrentMap
;
import
java.util.concurrent.ConcurrentHashMap
;
import
java.util.concurrent.atomic.AtomicInteger
;
import
javax.servlet.ServletContext
;
public
class
Predictions
{
private
ConcurrentMap
<
Integer
,
Prediction
>
predictions
;
![]()
private
ServletContext
sctx
;
private
AtomicInteger
mapKey
;
public
Predictions
()
{
predictions
=
new
ConcurrentHashMap
<
Integer
,
Prediction
>();
mapKey
=
new
AtomicInteger
();
}
public
void
setServletContext
(
ServletContext
sctx
)
{
this
.
sctx
=
sctx
;
}
public
ServletContext
getServletContext
()
{
return
this
.
sctx
;
}
public
void
setMap
(
ConcurrentMap
<
String
,
Prediction
>
predictions
)
{
}
public
ConcurrentMap
<
Integer
,
Prediction
>
getMap
()
{
if
(
good2Go
())
return
this
.
predictions
;
else
return
null
;
}
public
int
addPrediction
(
Prediction
p
)
{
![]()
int
id
=
mapKey
.
incrementAndGet
();
p
.
setId
(
id
);
predictions
.
put
(
id
,
p
);
return
id
;
}
public
Prediction
getPrediction
(
int
id
)
{
return
predictions
.
get
(
id
);
}
public
List
<
Prediction
>
getPredictions
()
{
![]()
List
<
Prediction
>
list
;
if
(
good2Go
())
{
Object
[]
preds
=
predictions
.
values
().
toArray
();
Arrays
.
sort
(
preds
);
list
=
new
ArrayList
<
Prediction
>();
for
(
Object
obj
:
preds
)
list
.
add
((
Prediction
)
obj
);
return
list
;
}
else
return
null
;
}
private
boolean
good2Go
()
{
if
(
getServletContext
()
==
null
)
return
false
;
if
(
predictions
.
size
()
<
1
)
populate
();
return
true
;
}
private
void
populate
()
{
String
filename
=
"/WEB-INF/data/predictions.db"
;
InputStream
in
=
sctx
.
getResourceAsStream
(
filename
);
// Read the data into the array of Predictions.
if
(
in
!=
null
)
{
try
{
InputStreamReader
isr
=
new
InputStreamReader
(
in
);
BufferedReader
reader
=
new
BufferedReader
(
isr
);
int
i
=
0
;
String
record
=
null
;
while
((
record
=
reader
.
readLine
())
!=
null
)
{
String
[]
parts
=
record
.
split
(
"!"
);
Prediction
p
=
new
Prediction
();
p
.
setWho
(
parts
[
0
]);
p
.
setWhat
(
parts
[
1
]);
addPrediction
(
p
);
}
}
catch
(
IOException
e
)
{
}
}
}
}
The Predictions
class provides the supporting data structures, in particular a thread-safe
ConcurrentMap
(line 1), together with convenience methods such as the method getPredictions
(line 3), which
returns a sorted List<Prediction>
, and the method addPrediction
(line 2), which adds a newly
created Prediction
to the existing collection. The predictionsSOAP service
invokes these methods as needed. As in the earlier versions, the
service initializes the Prediction
collection from the predictions.db file
in the deployed WAR file.
There remains one more service-side class to discuss, the ServiceHashHandler
. Perhaps the
best way to clarify this handler, however, is to switch first to the client side. The
reason is that the service-side handler extracts and verifies a credential that a
client-side handler needs to inject into every SOAP request message. One motivation behind the
predictionsSOAP example is to
mimic the authentication scheme used in Amazon’s E-Commerce service.
The PredictionsClient
class (see Example 5-7) is a client against the predictionsSOAP service.
Example 5-7. The PredictionsClient
against the predictionsSOAP service
import
clientSOAP.PredictionsSOAP
;
import
clientSOAP.PredictionsSOAPService
;
import
clientSOAP.Prediction
;
import
clientSOAP.ClientHandlerResolver
;
import
java.util.List
;
public
class
PredictionsClient
{
public
static
void
main
(
String
[
]
args
)
{
if
(
args
.
length
<
2
)
{
System
.
err
.
println
(
"Usage: PredictionsClient <name> <key>"
);
![]()
return
;
}
new
PredictionsClient
().
runTests
(
args
[
0
],
args
[
1
]);
}
private
void
runTests
(
String
name
,
String
key
)
{
PredictionsSOAPService
service
=
new
PredictionsSOAPService
();
service
.
setHandlerResolver
(
new
ClientHandlerResolver
(
name
,
key
));
PredictionsSOAP
port
=
service
.
getPredictionsSOAPPort
();
getTests
(
port
);
postTest
(
port
);
getAllTest
(
port
);
// confirm the POST
deleteTest
(
port
,
33
);
// delete the just POSTed prediction
getAllTest
(
port
);
// confirm the POST
putTest
(
port
);
}
private
void
getTests
(
PredictionsSOAP
port
)
{
getAllTest
(
port
);
getOneTest
(
port
);
}
private
void
getAllTest
(
PredictionsSOAP
port
)
{
msg
(
"getAll"
);
List
<
Prediction
>
preds
=
port
.
getAll
();
for
(
Prediction
pred
:
preds
)
System
.
out
.
println
(
String
.
format
(
"%2d: "
,
pred
.
getId
())
+
pred
.
getWho
()
+
" predicts: "
+
pred
.
getWhat
());
}
private
void
getOneTest
(
PredictionsSOAP
port
)
{
msg
(
"getOne (31)"
);
System
.
out
.
println
(
port
.
getOne
(
31
).
getWhat
());
}
private
void
postTest
(
PredictionsSOAP
port
)
{
msg
(
"postTest"
);
String
who
=
"Freddy"
;
String
what
=
"Something bad may happen."
;
String
res
=
port
.
create
(
who
,
what
);
System
.
out
.
println
(
res
);
}
private
void
putTest
(
PredictionsSOAP
port
)
{
msg
(
"putTest -- here's the record to be edited"
);
getOneTest
(
port
);
msg
(
"putTest results"
);
String
who
=
"FooBar"
;
String
what
=
null
;
// shouldn't change
int
id
=
31
;
String
res
=
port
.
edit
(
id
,
who
,
what
);
System
.
out
.
println
(
res
);
System
.
out
.
println
(
"Confirming:"
);
Prediction
p
=
port
.
getOne
(
31
);
System
.
out
.
println
(
p
.
getWho
());
System
.
out
.
println
(
p
.
getWhat
());
}
private
void
deleteTest
(
PredictionsSOAP
port
,
int
id
)
{
msg
(
"deleteTest"
);
String
res
=
port
.
delete
(
id
);
System
.
out
.
println
(
res
);
}
private
void
msg
(
String
s
)
{
System
.
out
.
println
(
"\n"
+
s
+
"\n"
);
}
}
As usual, the PredictionsClient
uses wsimport-generated artifacts, which are in the clientSOAP
package. This client, together
with dependencies, is packaged in the executable JAR file PredictionsClient.jar:
%
java
-
jar
PredictionsClient
.
jar
Usage:
PredictionsClient
<
name
>
<
key
>
The client expects two command-line arguments (line 1): a name (in Amazon E-Commerce, the accessId) and a key (in Amazon
E-Commerce, the secretKey). The predictionsSOAP service includes a DataStore
class that mimics a database with a
map with names as the lookup keys and secret keys as their values. Accordingly, the command:
%
java
-
jar
PredictionsClient
.
jar
moe
MoeMoeMoe
provides the required pair of command-line arguments, with moe as the name and MoeMoeMoe as the key.
The PredictionsClient
dynamically sets the client-side handler whose job is to turn the
command-line arguments into a credential that the service-side handler can verify. Here is the
relevant code segment:
PredictionsSOAPService
service
=
new
PredictionsSOAPService
();
service
.
setHandlerResolver
(
new
ClientHandlerResolver
(
name
,
key
));
![]()
PredictionsSOAP
port
=
service
.
getPredictionsSOAPPort
();
In line 1, name
and key
are the two command-line arguments. After setting the handler, the
PredictionsClient
runs the expected tests against the CRUD operations that the
predictionsSOAP service implements: getAll, getOne, create, edit, and delete. It should
be noted that the PredictionsClient
, like the predictionsSOAP service, does absolutely no
XML processing but instead works exclusively with Java data structures such as List<Prediction>
.
The ClientHandlerResolver
class (see Example 5-8) registers an instance of the class ClientHashHandler
with the
runtime system. Before digging into the details, it may be helpful to do a before/after
comparison with respect to the handler.
Example 5-8. The ClientHandlerResolver
and ClientHashHandler
classes
package
clientSOAP
;
import
java.text.SimpleDateFormat
;
import
java.util.ArrayList
;
import
java.util.Calendar
;
import
java.util.List
;
import
java.util.Set
;
import
java.util.TimeZone
;
import
javax.crypto.Mac
;
import
javax.crypto.spec.SecretKeySpec
;
import
javax.xml.namespace.QName
;
import
javax.xml.soap.SOAPMessage
;
import
javax.xml.soap.SOAPEnvelope
;
import
javax.xml.soap.SOAPHeader
;
import
javax.xml.ws.handler.Handler
;
import
javax.xml.ws.handler.HandlerResolver
;
import
javax.xml.ws.handler.MessageContext
;
import
javax.xml.ws.handler.PortInfo
;
import
javax.xml.ws.handler.soap.SOAPHandler
;
import
javax.xml.ws.handler.soap.SOAPMessageContext
;
import
org.apache.commons.codec.binary.Base64
;
import
org.w3c.dom.Element
;
import
org.w3c.dom.Node
;
public
class
ClientHandlerResolver
implements
HandlerResolver
{
private
String
name
;
private
String
key
;
public
ClientHandlerResolver
(
String
name
,
String
key
)
{
this
.
name
=
name
;
this
.
key
=
key
;
}
public
List
<
Handler
>
getHandlerChain
(
PortInfo
portInfo
)
{
List
<
Handler
>
handlerChain
=
new
ArrayList
<
Handler
>();
handlerChain
.
add
(
new
ClientHashHandler
(
this
.
name
,
this
.
key
));
return
handlerChain
;
}
}
class
ClientHashHandler
implements
SOAPHandler
<
SOAPMessageContext
>
{
private
byte
[
]
secretBytes
;
private
String
name
;
public
ClientHashHandler
(
String
name
,
String
key
)
{
this
.
name
=
name
;
this
.
secretBytes
=
getBytes
(
key
);
}
public
void
close
(
MessageContext
mCtx
)
{
}
![]()
public
Set
<
QName
>
getHeaders
()
{
return
null
;
}
![]()
public
boolean
handleFault
(
SOAPMessageContext
mCtx
)
{
![]()
try
{
SOAPMessage
msg
=
mCtx
.
getMessage
();
msg
.
writeTo
(
System
.
err
);
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
e
);
}
return
true
;
}
public
boolean
handleMessage
(
SOAPMessageContext
mCtx
)
{
![]()
Boolean
outbound
=
(
Boolean
)
mCtx
.
get
(
MessageContext
.
MESSAGE_OUTBOUND_PROPERTY
);
if
(
outbound
)
{
![]()
try
{
SOAPMessage
soapMessage
=
mCtx
.
getMessage
();
SOAPEnvelope
envelope
=
soapMessage
.
getSOAPPart
().
getEnvelope
();
// Ensure there is a header and add a 'wrapper' element.
if
(
envelope
.
getHeader
()
==
null
)
envelope
.
addHeader
();
![]()
SOAPHeader
header
=
envelope
.
getHeader
();
QName
qn
=
new
QName
(
"http://predictionsSOAP"
,
"credentials"
);
header
.
addHeaderElement
(
qn
);
![]()
// Now insert credentials into the header.
String
timeStamp
=
getTimestamp
();
String
signature
=
getSignature
(
this
.
name
,
timeStamp
,
this
.
secretBytes
);
Node
firstChild
=
header
.
getFirstChild
();
append
(
firstChild
,
"Name"
,
this
.
name
);
![]()
append
(
firstChild
,
"Signature"
,
signature
);
![]()
append
(
firstChild
,
"Timestamp"
,
timeStamp
);
![]()
soapMessage
.
saveChanges
();
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
"SOAPException thrown."
,
e
);
}
}
return
true
;
// continue down the handler chain
}
private
String
getSignature
(
String
name
,
String
timestamp
,
byte
[
]
secretBytes
)
{
try
{
System
.
out
.
println
(
"Name == "
+
name
);
System
.
out
.
println
(
"Timestamp == "
+
timestamp
);
String
toSign
=
name
+
timestamp
;
byte
[]
toSignBytes
=
getBytes
(
toSign
);
Mac
signer
=
Mac
.
getInstance
(
"HmacSHA256"
);
SecretKeySpec
keySpec
=
new
SecretKeySpec
(
secretBytes
,
"HmacSHA256"
);
signer
.
init
(
keySpec
);
signer
.
update
(
toSignBytes
);
byte
[]
signBytes
=
signer
.
doFinal
();
String
signature
=
new
String
(
Base64
.
encodeBase64
(
signBytes
));
return
signature
;
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
"NoSuchAlgorithmException thrown."
,
e
);
}
}
private
String
getTimestamp
()
{
Calendar
calendar
=
Calendar
.
getInstance
();
SimpleDateFormat
dateFormat
=
new
SimpleDateFormat
(
"yyyy-MM-dd'T'HH:mm:ss'Z'"
);
dateFormat
.
setTimeZone
(
TimeZone
.
getTimeZone
(
"UTC"
));
return
dateFormat
.
format
(
calendar
.
getTime
());
}
private
void
append
(
Node
node
,
String
elementName
,
String
elementText
)
{
Element
element
=
node
.
getOwnerDocument
().
createElement
(
elementName
);
element
.
setTextContent
(
elementText
);
node
.
appendChild
(
element
);
}
private
byte
[
]
getBytes
(
String
str
)
{
try
{
return
str
.
getBytes
(
"UTF-8"
);
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
e
);
}
}
}
As an example, consider the first test
that the PredictionsClient
runs: the client invokes getAll
on the service to get a
list of all of the predictions. Here is what happens:
The underlying SOAP libraries generate the appropriate SOAP message:
<
S:
Envelope
xmlns:
S
=
"http://schemas.xmlsoap.org/soap/envelope/"
>
<
S:
Body
><
ns2:
getAll
xmlns:
ns2
=
"http://predictions/"
/></
S:
Body
>
</
S:
Envelope
>
This automatically generated SOAP message has no header.
Before this message is handed over to HTTP for transport to the service, the runtime
invokes the ClientHashHandler
, which amends the outgoing message to:
<
S:
Envelope
xmlns:
S
=
"http://schemas.xmlsoap.org/soap/envelope/"
>
<
S:
Header
>
<
credentials
xmlns
=
"http://predictionsSOAP"
>
<
Name
xmlns
=
""
>
moe
</
Name
>
<
Signature
xmlns
=
""
>
vkIQm8isdpomHeQ1aFqckzQawlZakj
+
hWOQaxsQcPgU
=
</
Signature
>
<
Timestamp
xmlns
=
""
>
2014
-
03
-
26
T01:
12
:
47
Z
</
Timestamp
>
</
credentials
>
</
S:
Header
>
<
S:
Body
>
<
ns2:
getAll
xmlns:
ns2
=
"http://predictions/"
/>
</
S:
Body
>
</
S:
Envelope
>
A SOAP header together with a credentials
element is added; the credentials
element
has three subelements tagged Name
, Signature
, and Timestamp
in that
lexicographical order. The Name
is the command-line argument moe
and the
Signature
is an HmacSHA256
hash encoded in base64, the same kind of hash
used in Amazon’s E-Commerce service. The Signature
hash is generated from the
provided key value MoeMoeMoe
, but this secretKey cannot be recovered from the
hash. (Chapter 6, on security, explains why.) Accordingly, Moe’s secret key
is not in jeopardy of being hijacked when the SOAP request is sent over the wire.
The ClientHashHandler
class implements the SOAPHandler
interface and, therefore,
defines the four methods getHeaders
, close
, handleFault
, and
handleMessage
(lines 1 through 4 in the code listing). Only handleFault
and
handleMessage
are of interest here. Recall that handlers are inherently
bidirectional; that is, they handle incoming and outgoing messages alike, and
only one of these methods is invoked in either case: if there is a fault, the
runtime invokes handleFault
; otherwise, the runtime invokes handleMessage.
The handleMessage
method has work to do only on outgoing messages or
requests; hence, this method checks the direction of the message (line 5). If
the message is indeed outgoing, the handler does the following:
credentials
(line 7).
credentials
element, three elements tagged Name
(with a
value such as moe
), Signature
(whose value is an HmacSHA256
hash generated with,
in this case, Moe’s secret key), and Timestamp
(whose value is a properly formatted
timestamp).
The outgoing SOAP message, a request, is now properly structured. The SOAP body names
the operation of interest (for instance, getOne
) and includes any required
arguments (in this example, the integer identifier of the Prediction
to get). The SOAP
header contains the requester’s name, a hash value that serves as a signature, and a
timestamp.
The handler method handleFault
does not check the message direction because a
fault would arrive, in any case, as a response from the predictionsSOAP service. This
service generates a SOAP fault as a VerbosityException
if a candidate Prediction
is excessively wordy.
At present, handleFault
simply prints the SOAP fault to the standard error; in a production
environment, more elaborate logic might be brought into play—for instance, the fault might be
saved in a data store for later analysis.
Handlers, especially SOAP handlers, are powerful in that they can amend the SOAP message
created at the application level. In this example, a SOAP
rather than a Logical
handler is
needed because the handler needs access to the SOAP header.
On the service side, the handler also
needs to be a SOAP handler.
The ServiceHashHandler
(see Example 5-9) is a service-side SOAP handler. On any incoming message,
this handler checks for the following:
credentials
? If not,
generate a SOAP fault (line 2).
Name
, Signature
, and Timestamp
values all non-null? If not,
generate a SOAP fault (line 3).
Name
, used as a lookup key in the service-side data store, have a value? (The value
should be the user’s secret key.) If not, generate a SOAP fault (line 4).
Signature
generated on the service side match the
Signature
sent in the client request? If not, generate a SOAP fault (line 5).
Example 5-9. The service-side ServiceHashHandler
, which verifies the credentials in a request
package
predictions
;
import
java.text.SimpleDateFormat
;
import
java.util.ArrayList
;
import
java.util.Calendar
;
import
java.util.List
;
import
java.util.Set
;
import
java.util.Map
;
import
java.util.HashMap
;
import
java.util.TimeZone
;
import
java.util.Iterator
;
import
java.util.Arrays
;
import
javax.crypto.Mac
;
import
javax.crypto.spec.SecretKeySpec
;
import
javax.xml.namespace.QName
;
import
javax.xml.soap.SOAPHeader
;
import
javax.xml.soap.SOAPBody
;
import
javax.xml.soap.SOAPFault
;
import
javax.xml.soap.SOAPMessage
;
import
javax.xml.ws.soap.SOAPFaultException
;
import
javax.xml.soap.SOAPException
;
import
javax.xml.ws.handler.Handler
;
import
javax.xml.ws.handler.MessageContext
;
import
javax.xml.ws.handler.soap.SOAPHandler
;
import
javax.xml.ws.handler.soap.SOAPMessageContext
;
import
org.apache.commons.codec.binary.Base64
;
import
org.w3c.dom.Node
;
import
org.w3c.dom.NodeList
;
public
class
ServiceHashHandler
implements
SOAPHandler
<
SOAPMessageContext
>
{
private
byte
[
]
secretBytes
;
public
ServiceHashHandler
()
{
}
public
void
close
(
MessageContext
mCtx
)
{
}
public
Set
<
QName
>
getHeaders
()
{
return
null
;
}
public
boolean
handleFault
(
SOAPMessageContext
mCtx
)
{
return
true
;
}
public
boolean
handleMessage
(
SOAPMessageContext
mCtx
)
{
Boolean
outbound
=
(
Boolean
)
mCtx
.
get
(
MessageContext
.
MESSAGE_OUTBOUND_PROPERTY
);
if
(!
outbound
)
{
try
{
SOAPMessage
msg
=
mCtx
.
getMessage
();
SOAPHeader
soapHeader
=
msg
.
getSOAPHeader
();
if
(
soapHeader
==
null
)
![]()
generateFault
(
msg
,
"No header!"
);
Node
node
=
soapHeader
.
getFirstChild
();
// credentials
NodeList
nodeList
=
node
.
getChildNodes
();
// Name, Timestamp, Sig.
if
(
nodeList
.
getLength
()
<
3
)
![]()
generateFault
(
msg
,
"Too few header nodes!"
);
// Extract the required attributes.
String
name
=
nodeList
.
item
(
0
).
getFirstChild
().
getNodeValue
();
String
signature
=
nodeList
.
item
(
1
).
getFirstChild
().
getNodeValue
();
String
timestamp
=
nodeList
.
item
(
2
).
getFirstChild
().
getNodeValue
();
if
(
name
==
null
||
timestamp
==
null
||
signature
==
null
)
![]()
generateFault
(
msg
,
"Missing header key/value pairs!"
);
// Generate comparison signature and compare against what's sent.
String
secret
=
DataStore
.
get
(
name
);
if
(
secret
==
null
)
generateFault
(
msg
,
name
+
" not registered!"
);
![]()
byte
[
]
secretBytes
=
getBytes
(
secret
);
String
localSignature
=
getSignature
(
name
,
timestamp
,
secretBytes
);
if
(!
verify
(
signature
,
localSignature
))
generateFault
(
msg
,
"HMAC signatures do not match."
);
![]()
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
"SOAPException thrown."
,
e
);
}
}
return
true
;
// continue down the handler chain
}
private
boolean
verify
(
String
sig1
,
String
sig2
)
{
return
Arrays
.
equals
(
sig1
.
getBytes
(),
sig2
.
getBytes
());
}
private
String
getSignature
(
String
name
,
String
timestamp
,
byte
[
]
secretBytes
)
{
try
{
System
.
err
.
println
(
"Name == "
+
name
);
System
.
err
.
println
(
"Timestamp == "
+
timestamp
);
String
toSign
=
name
+
timestamp
;
byte
[]
toSignBytes
=
getBytes
(
toSign
);
Mac
signer
=
Mac
.
getInstance
(
"HmacSHA256"
);
SecretKeySpec
keySpec
=
new
SecretKeySpec
(
secretBytes
,
"HmacSHA256"
);
signer
.
init
(
keySpec
);
signer
.
update
(
toSignBytes
);
byte
[]
signBytes
=
signer
.
doFinal
();
String
signature
=
new
String
(
Base64
.
encodeBase64
(
signBytes
));
return
signature
;
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
"NoSuchAlgorithmException thrown."
,
e
);
}
}
private
String
getTimestamp
()
{
Calendar
calendar
=
Calendar
.
getInstance
();
SimpleDateFormat
dateFormat
=
new
SimpleDateFormat
(
"yyyy-MM-dd'T'HH:mm:ss'Z'"
);
dateFormat
.
setTimeZone
(
TimeZone
.
getTimeZone
(
"UTC"
));
return
dateFormat
.
format
(
calendar
.
getTime
());
}
private
byte
[
]
getBytes
(
String
str
)
{
try
{
return
str
.
getBytes
(
"UTF-8"
);
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
e
);
}
}
private
void
generateFault
(
SOAPMessage
msg
,
String
reason
)
{
try
{
SOAPBody
body
=
msg
.
getSOAPBody
();
SOAPFault
fault
=
body
.
addFault
();
fault
.
setFaultString
(
reason
);
throw
new
SOAPFaultException
(
fault
);
}
catch
(
SOAPException
e
)
{
}
}
}
The predictionsSOAP service does signature verification in basically the same way that Amazon does.
To make a request against the predictionsSOAP service, a client needs a key. How this is distributed
to the client is ignored in this example. In the Amazon case, the secretKey is provided when a user
registers with Amazon, and, of course, Amazon maintains a copy of the secretKey. In the predictionsSOAP
example, the service-side DataStore
has a map whose lookup keys are usernames (for instance, Moe) and
whose values are the users’ secret keys (in this case, MoeMoeMoe). On an incoming message, the ServiceHashHandler
recomputes the hash value—generated on the client side with the user’s key—and then does a
byte-by-byte comparison of the sent signature and the signature computed on the service side. The
code is in the verify
utility method:
private
boolean
verify
(
String
sig1
,
String
sig2
)
{
return
Arrays
.
equals
(
sig1
.
getBytes
(),
sig2
.
getBytes
());
}
The argument sig1
is the sent signature, and the argument sig2
is the signature computed on the service side.
The API for generating a SOAP fault at the handler level differs significantly from the API for generating a SOAP fault at the application level. At the application level, the regular Java syntax of:
throw
new
VerbosityException
(...);
suffices; at the handler level,
by contrast, the SOAP fault needs to be constructed and then thrown. Here again is the
generateFault
method in the ServiceHashHandler
:
private
void
generateFault
(
SOAPMessage
msg
,
String
reason
)
{
try
{
SOAPBody
body
=
msg
.
getSOAPBody
();
![]()
SOAPFault
fault
=
body
.
addFault
();
![]()
fault
.
setFaultString
(
reason
);
![]()
throw
new
SOAPFaultException
(
fault
);
![]()
}
catch
(
SOAPException
e
)
{
}
}
The generateFault
method uses the incoming SOAP message (msg
is the reference) to
get the SOAP body (line 1). A SOAP fault is then added to body
(line 2); the reason
for the fault is
given; and a SOAPFaultException
, initialized with the fault information (line 3), is thrown—which
in turn causes a SOAP fault message to be sent back to
the requester (line 4). If desired, additional Detail
could be added to the SOAPFault
, which has
an addDetail
method.
The service-side handler ServiceHashHandler
needs to be linked to the service itself, whose
@WebService
-annotated class is PredictionsSOAP
. On the client side, the linking is dynamic. Here, for
review, are the two critical lines of code in the PredictionsClient
:
PredictionsSOAPService
service
=
new
PredictionsSOAPService
();
![]()
service
.
setHandlerResolver
(
new
ClientHandlerResolver
(
name
,
key
));
Line 2 in the listing performs the dynamic linking. With Tomcat deployment, this option is
not available. Instead, the predictionsSOAP service and the ServiceHashHandler
are linked through an XML configuration file encapsulated in the deployed WAR file:
<
handler
-
chains
xmlns
=
"http://java.sun.com/xml/ns/javaee"
>
<
handler
-
chain
>
![]()
<
handler
>
<
handler
-
name
>
predictions
.
ServiceHashHandler
</
handler
-
name
>
![]()
<
handler
-
class
>
predictions
.
ServiceHashHandler
</
handler
-
class
>
</
handler
>
</
handler
-
chain
>
</
handler
-
chains
>
The name of the configuration file is arbitrary. A handler-chain
(line 1) can include
arbitrarily many handler
instances but, in this case, there is but one handler
in the handler-chain
, the handler ServiceHashHandler
(line 2). This configuration file
then is referenced with a @HandlerChain
annotation in the PredictionsSOAP
class:
@WebService
@HandlerChain
(
file
=
"../../../WEB-INF/serviceHandler.xml"
)
public
class
PredictionsSOAP
{
...
The serviceHandler.xml file winds up in the WEB-INF directory of the deployed WAR file because the usual Ant script is used to deploy the predictionsSOAP service to Tomcat.
The configuration document serviceHandler.xml shown above indicates, with its handler-chain
tag,
that multiple handlers might be in play on either the service or the client side. Figure 5-6
depicts the structure of such a chain. For an outgoing message, logical handlers come into
play first. This is appropriate because such handlers have limited scope; that is, they have
access only to the payload in the SOAP body. The (SOAP) message handlers then come into play
and these handlers, as noted earlier, have access to the entire SOAP message. For an incoming
message, the order is reversed: the (SOAP) message handlers have first access and the
logical handlers have last access. Message handlers are sufficient for any handler logic
precisely because they have access to the entire SOAP message, but logical handlers are
convenient in that the runtime makes available only the SOAP body’s payload.
The next section adds a second handler, in this case a LogicalHandler
, to the
client side of the predictionsSOAP service. The result is a true chain of handlers.