In this article I will build upon the basic tenets of Web services. If you do not have a good understanding of UDDI, WSDL, and Axis, or if you haven't created a basic Web service before, then you will probably not find this article very useful. If this is the case then I would suggest you start with some resources which explain Web services and the associated technologies. On the other hand, if you do understand Web services and are confused or unsure about any of the material presented in this article please post a comment in the Web Services Technical forum, which I will monitor on a regular basis.
Although this article is not about the server-side implementation of a Web service, it is worth mentioning the makeup of the Web service I will use. I will base my Web service upon a simple Java class with a method that takes two integers as parameters and returns an integer which is the multiplication of the parameters. This class will, of course, be exposed as a Web service, and that service will need to be published to UDDI.
If you are using the WebSphere SDK for Web Services (WSDK) then simply download the server side implementation (see the Resources section), which I've already created for you, and follow these steps:
- Unzip the downloaded file into the WSDK's services\applications directory. (that is, services\applications\WebMath).
- Run
wsdkconfigand install WebMath. - Start up the application server.
- Type run publish at the command line from the services\applications\WebMath\client directory.
And that's it! The Web service is published to UDDI and waiting for client requests.
If you don't have the Web Sphere SDK for Web services it can be downloaded for free (see the Resources section). Otherwise you will need to create the above described Web service from scratch on whichever platform you want to use. Just be sure that when it's published to UDDI the business entity is "Damian Hagge Ltd." and that the business service is "WebMathService". If you do this, then the client code provided in the package for this article (see Resources) should work like a charm.
I'm now ready to talk about the interesting part -- dynamic discovery and invocation.
The code I will discuss is DynamicInvoke.java
and can be found in the
WebMath\client directory of the
download.
Although this code is somewhat lengthy, I will endeavor to explain it as concisely as possible.
So what do I need to do to dynamically discover and invoke the service? Well, first I will need to discover information about the service from UDDI. Second, I will need to read the WSDL implementation file from the service provider and parse it for various information. Finally, I will have enough information to marshal (encode) a SOAP request to the Web service implementation using Axis.
In the client code this is accomplished using the open source packages
uddi4j, wsdl4j,
and Apache Axis. I will use uddi4j
to browse the UDDI registry as it provides an API which allows the user to make inquiries
and publish to any UDDI 2.0 registry. The wsdl4j package will
be used to programmatically represent the elements of a WSDL file so I can browse and
glean various information from it. Then I will use Axis
to actually make the SOAP request to the server and wait for the response.
Finding the Web service in UDDI
In order to dynamically invoke your Web service you need to know four things. You must know the inquiry URL of the UDDI registry, the publish URL of the UDDI, the name of the business entity, and the name of the business service. This information can be found in Listing 1.
Listing 1. DynamicInvoke.java - line 38
public class DynamicInvoke {
private String uddiInquiryURL = "http://localhost:80/uddisoap/inquiryapi";
private String uddiPublishURL = "http://localhost:80/uddisoap/publishapi";
private String businessName = "Damian Hagge Ltd.";
private String serviceName = "WebMathService";
|
This says where the UDDI is, which business entity to look for, and once that is found, which business service to select. I use the UDDI URLs to create a proxy to the UDDI registry which I then use to query the registry to find the business service. This is done as explained in Listing 2.
Listing 2. DynamicInvoke.java - line 65
// create a proxy to the UDDI
UDDIProxy proxy = new UDDIProxy(
new URL(uddiInquiryURL), new URL(uddiPublishURL));
// we need to find the business in the UDDI
// we must first create the Vector of business name
Vector names = new Vector();
names.add(new Name(businessName));
// now get a list of all business matching our search criteria
BusinessList businessList =
proxy.find_business(names, null, null, null, null, null,10);
// now we need to find the BusinessInfo object for our business
Vector businessInfoVector =
businessList.getBusinessInfos().getBusinessInfoVector();
BusinessInfo businessInfo = null;
for (int i = 0; i < businessInfoVector.size(); i++) {
businessInfo = (BusinessInfo)businessInfoVector.elementAt(i);
// make sure we have the right one
if(businessName.equals(businessInfo.getNameString())) {
break;
}
} |
Here you can see that I created a UDDIProxy. Then I created a
Vector containing the desired business name and passed it in as a
parameter to UDDIProxy.find_business. This call returned a
BusinessList object which contains a
BusinessInfos object. BusinessInfos
simply contains a vector of BusinessInfo objects. This is a
common trait of the uddi4j API where one object has a reference
to another object that simply wraps a vector. This programmatic pattern is used throughout
the client code so, throughout the rest of this article, I won't explain the
semantics of code which simply finds the classes in a vector that is wrapped by a
parent object.
I then iterate through all the BusinessInfo objects
to find the one which has the name Damian Hagge Ltd.. This ensures that I
have the right business entity.
Now I need to find the business service with the name WebMathService. This is
done by searching all the BusinessService object's
referenced, albeit indirectly, by the BusinessInfo object.
When I find the BusinessService with the name
WebMathService, I have the right class. The code that does this is in Listing 3.
Listing 3. DynamicInvoke.java - line 88
// now find the service info
Vector serviceInfoVector =
businessInfo.getServiceInfos().getServiceInfoVector();
ServiceInfo serviceInfo = null;
for (int i = 0; i < serviceInfoVector.size(); i++) {
serviceInfo = (ServiceInfo)serviceInfoVector.elementAt(i);
// make sure we have the right one
if(serviceName.equals(serviceInfo.getNameString())) {
break;
}
}
// we now need to get the business service object for our service
// we do this by getting the ServiceDetail object first, and
// getting the BusinessService objects through it
ServiceDetail serviceDetail = proxy.get_serviceDetail(
serviceInfo.getServiceKey());
Vector businessServices = serviceDetail.getBusinessServiceVector();
BusinessService businessService = null;
for (int i = 0; i < businessServices.size(); i++) {
businessService = (BusinessService)businessServices.elementAt(i);
// make sure we have the right one
if(serviceName.equals(businessService.getDefaultNameString())) {
break;
}
} |
Again, you can see the programmatic pattern of one object referencing another object that simply contains a vector of the object(s) I am really looking for.
Now that I have the business service, I need to somehow glean the URI of the WSDL implementation on the service provider's machine. This will be represented by the Overview URL element of the tModel in the UDDI registry. To find this I must first find the business service's binding template which contains an http-based access point. Once I've found this, then I can find the appropriate tModel which is referenced by the binding template. Programmatically, I do this listed in Listing 4.
Listing 4. DynamicInvoke.java - line 115
// ok, now we have the business service so we can get the binding template
Vector bindingTemplateVector =
businessService.getBindingTemplates().getBindingTemplateVector();
AccessPoint accessPoint = null;
BindingTemplate bindingTemplate = null;
for(int i=0; i<bindingTemplateVector.size(); i++) {
// find the binding template with an http access point
bindingTemplate = (BindingTemplate)bindingTemplateVector.elementAt(i);
accessPoint = bindingTemplate.getAccessPoint();
if(accessPoint.getURLType().equals("http")) {
break;
}
}
// ok now we know which binding template we're dealing with
// we can now find out the overview URL
Vector tmodelInstanceInfoVector =
bindingTemplate.getTModelInstanceDetails().getTModelInstanceInfoVector();
String wsdlImplURI = null;
for(int i=0; i<tmodelInstanceInfoVector.size(); i++) {
TModelInstanceInfo instanceInfo =
(TModelInstanceInfo)tmodelInstanceInfoVector.elementAt(i);
InstanceDetails details = instanceInfo.getInstanceDetails();
OverviewDoc wsdlImpl = details.getOverviewDoc();
wsdlImplURI = wsdlImpl.getOverviewURLString();
if(wsdlImplURI != null) break;
} |
In the first code block I looked at all the binding templates referenced by
the business service and found the one which had a URL type of "http". I then
iterated through all the tModel's references by the binding template and stopped when
I found the first overview URL (that is, location of WSDL implementation). Now that I
know the URL of the WSDL implementation, I can proceed to use
wsdl4j to request the document directly from the service
provider and parse it for the parameters I need to pass to Axis
in order to invoke the service.
Note: Depending on the implementation of UDDI 2.0-compliant registry you are using, the overview URL contained in the tModel might not point to the WSDL implementation. If this is the case, then you will need to do more investigating as to how you can glean the location of the service provider's WSDL implementation from either UDDI or the location where the overview URL points.
Now that you have the URL for the Web service's WSDL implementation you can use
wsdl4j to parse it. In order to call the
service using Axis you will need to glean the
following information from the WSDL:
- the target namespace
- the service name
- the port name
- the operation name
- the operations input parameters
In my example, in order to glean this information, I must first obtain
Definition objects for both the WSDL implementation and the
WSDL interface. My code is in Listing 5.
Listing 5. DynamicInvoke.java - line 159
// first get the definition object got the WSDL impl
try {
WSDLFactory factory = new WSDLFactoryImpl();
WSDLReader reader = factory.newWSDLReader();
implDef = reader.readWSDL(implURI);
} catch(WSDLException e) {
e.printStackTrace();
}
if(implDef==null) {
throw new WSDLException(
WSDLException.OTHER_ERROR,"No WSDL impl definition found.");
}
// now get the Definition object for the interface WSDL
Map imports = implDef.getImports();
Set s = imports.keySet();
Iterator it = s.iterator();
while(it.hasNext()) {
Object o = it.next();
Vector intDoc = (Vector)imports.get(o);
// we want to get the ImportImpl object if it exists
for(int i=0; i<intDoc.size(); i++) {
Object obj = intDoc.elementAt(i);
if(obj instanceof ImportImpl) {
interfaceDef = ((ImportImpl)obj).getDefinition();
}
}
}
|
As you can see, I obtain a Definition object for the
WSDL implementation from the implementation URL I gleaned while perusing the UDDI registry.
I then obtained another Definition object, this one representing
the WSDL interface, by searching all the imports defined in the implementation WSDL. A
well-formed implementation WSDL should have an import pointing to its corresponding
WSDL interface.
It's simple now to find the target namespace which to be passed to
Axis. I just make a call to
Definition.getTargetNamespace() on the WSDL implementation object
and that's it. Next, let's find the port which contains the operations I want to invoke.
This is done in Listing 6.
Listing 6. DynamicInvoke.java - line 195
// great we've got the WSDL definitions now we need to find the PortType so
// we can find the methods we can invoke
Vector allPorts = new Vector();
Map ports = interfaceDef.getPortTypes();
s = ports.keySet();
it = s.iterator();
while(it.hasNext()) {
Object o = it.next();
Object obj = ports.get(o);
if(obj instanceof PortType) {
allPorts.add((PortType)obj);
}
}
// now we've got a vector of all the port types - normally some logic would
// go here to choose which port type we want to use but we'll just choose
// the first one
PortType port = (PortType)allPorts.elementAt(0);
List operations = port.getOperations();
|
I get all the ports declared in the interface WSDL and just choose the first one. Of course if this were a real-world application I would want to develop some type of algorithm to choose the desired port and operation. However, in the interest of brevity, I won't do this.
Now that I've chosen the port to use, I find the service name and port name which
I will supply to Axis. I do this by finding the
binding in the WSDL interface which corresponds to the chosen port. Then
I glean the service and port in the WSDL implementation that provides an
endpoint for this binding.
You may find this a bit overcomplicated, so I will simplify it. Take a look at the
WebMath_Impl.wsdl file and the
WebMath_Interface.wsdl file which can be found in the
WebMath\webapp directory. You can see in the implementation file
that there is a <service> tag which contains a <port> tag. In the interface file you
can see that there is a <binding> tag which contains an <operation> and also
a <port> tag which represents the port I have already chosen. In both of
these files there could feasibly be several of each of these so what I need to do is ensure
that the service I choose in the implementation file contains the port which refers to the
binding which contains the chosen port in the interface file.
In other words, I need to make sure that the binding I choose in the interface
WSDL contains the same port I have already chosen (in the code snippet above). Then I
need to make sure that the service chosen in the implementation WSDL contains a port
which has a binding attribute value which is the same as the interface WSDL
binding's name attribute value. If these are the same, then I've found
the service and binding which refer to my chosen port.
See what the above algorithm looks like in Listing 7.
Listing 7. DynamicInvoke.java - line 215
// let's get the service in the WSDL impl which contains this port
// to do this we must first find the QName of the binding with the
// port type that corresponds to the port type of our chosen part
QName bindingQName = null;
Map bindings = interfaceDef.getBindings();
s = bindings.keySet();
it = s.iterator();
while(it.hasNext()) {
Binding binding = (Binding)bindings.get(it.next());
if(binding.getPortType()==port) {
// we've got our binding
bindingQName = binding.getQName();
}
}
if(bindingQName==null) {
throw new WSDLException(WSDLException.OTHER_ERROR,
"No binding found for chosen port type.");
}
// now we can find the service in the WSDL impl which provides an
// endpoint for the service we just found above
Map implServices = implDef.getServices();
s = implServices.keySet();
it = s.iterator();
while(it.hasNext()) {
Service serv = (Service)implServices.get(it.next());
Map m = serv.getPorts();
Set set = m.keySet();
Iterator iter = set.iterator();
while(iter.hasNext()) {
Port p = (Port)m.get(iter.next());
if(p.getBinding().getQName().toString().equals(
bindingQName.toString())) {
// we've got our service store the port name and service name
portName = serv.getQName().toString();
serviceName = p.getName();
break;
}
}
if(portName != null) break;
}
|
By executing the above code I have gleaned the port name and service name that I need to pass to Axis. Now the only additional pieces of
information I need are the method name and the parameters to pass into the method call.
Discovering the parameters and the Java type (that is, Java class, or native type) they map to is done in Listing 8.
Listing 8. DynamicInvoke.java - line 256
// ok now we got all the operations previously - normally we would have some
logic here to
// choose which operation, however, for the sake of simplicity we'll just
// choose the first one
Operation op = (Operation)operations.get(0);
operationName = op.getName();
// now let's get the Message object describing the XML for the input and output
// we don't care about the specific type of the output as we'll just cast it to
an Object
Message inputs = op.getInput().getMessage();
// let's find the input params
Map inputParts = inputs.getParts();
// create the object array which Axis will use to pass in the parameters
inputParams = new Object[inputParts.size()];
s = inputParts.keySet();
it = s.iterator();
int i=0;
while(it.hasNext()) {
Part part = (Part)inputParts.get(it.next());
QName qname = part.getTypeName();
// if it's not in the http://www.w3.org/2001/XMLSchema namespace then
// we don't know about it - throw an exception
String namespace = qname.getNamespaceURI();
if(!namespace.equals("http://www.w3.org/2001/XMLSchema")) {
throw new WSDLException(
WSDLException.OTHER_ERROR,"Namespace unrecognized");
}
// now we can get the Java type which the the QName maps to
// we do this by using the Axis tools which map WSDL types
// to Java types in the wsdl2java tool
String localPart = qname.getLocalPart();
javax.xml.rpc.namespace.QName wsdlQName =
new javax.xml.rpc.namespace.QName(namespace,localPart);
TypeMapping tm = DefaultTypeMappingImpl.create();
Class cl = tm.getClassForQName(wsdlQName);
// if the Java type is a primitive, we need to wrap it in an object
if(cl.isPrimitive()) {
cl = wrapPrimitive(cl);
}
// we could prompt the user to input the param here but we'll just
// assume a random number between 1 and 10. First we need to
// find the constructor which takes a string representation of a number
// if a complex type was required we would use reflection to break it
// down and prompt the user to input values for each member variable
// in Object representing the complex type
try {
Constructor cstr = cl.getConstructor(
new Class[] { Class.forName("java.lang.String") });
inputParams[i] = cstr.newInstance(
new Object [] { ""+new Random().nextInt(10) });
} catch(Exception e) {
// shoudn't happen
e.printStackTrace();
}
i++;
}
|
Let me break down what's happening here. I already have an
Operation object which I got from the call to
Port.getOperations() I made on the port object. From this a
simple call to Operation.getName() will yield the method name.
I now need to get the Message object for the
<input> tag which is referred to by the chosen port. The <input> tag specifies
the parameters that should be passed in to the method call. This is done by the code
Operation.getInput().getMessage(). Once I've gleaned this, I then need to find all the parts the Message object contains.
I iterate through the parts and make sure that the namespace is the XML Schema namespace, so I'm sure I can map the part type to Java code. In a real-world application, of course, this would be expanded to support other types possibly as well.
I then switch over to a little bit of Axis in order to find
to which Java class the type maps. Axis contains a
TypeMapping.getClassForQName() method which is used in its
wsdl2java tool. All I need to do is create a
javax.xml.rpc.namespace.QName object which represents the
type. Once I've made the call, then I have a Java Class object
representing the type.
If the Class is a primitive type then I need to wrap it in a
Java object. The code to do this is not presented here since it is rather simple, however,
if you want to look at it it can be found in DynamicInvoke.java.When I have this Java object I will pass to Axis, I need to instantiate it, supply a value to the constructor and then store it. I find the constructor for the object using reflection and instantiate it providing a random number between 1 and 10.
Of course, if this were more than a demo some other logic would choose the parameter, perhaps
prompting the user to supply the information. Finally,
I store the instantiated object in an Object array.
Phew! I've gathered all the information needed to invoke the service. Now I can
use an Axis call to invoke the Web service.
Invoking the Web service with Axis
Invoking the Web service using
Axis is a relatively straightforward procedure which is
coded in Listing 9.
Listing 9. DynamicInvoke.java - line 365
public void axisInvoke(String targetNamespace, String serviceName,
String portName, String operationName, Object[] inputParams,
String implURI) {
try {
// first, due to a funny Axis idiosyncrasy we must strip portName of
// it's target namespace so we can pass it in as
// targetNamespace, localPart
int index = portName.indexOf(":",
portName.indexOf("http://")+new String("http://").length());
String portNamespace = portName.substring(0,index);
portName = portName.substring(
index==0?index:index+1); // to strip the :
javax.xml.rpc.namespace.QName serviceQN =
new javax.xml.rpc.namespace.QName( portNamespace, portName );
org.apache.axis.client.Service service =
new org.apache.axis.client.Service(new URL(implURI), serviceQN);
javax.xml.rpc.namespace.QName portQN =
new javax.xml.rpc.namespace.QName( targetNamespace, serviceName );
// This Call object will be used the invocation
Call call = (Call) service.createCall();
// Now make the call...
System.out.println("Invoking service>> " + serviceName + " <<...");
call.setOperation( portQN, operationName );
Object ret = (Integer) call.invoke( inputParams );
System.out.println("Result returned from call to "+
serviceName+" -- "+ret);
} catch(java.net.MalformedURLException e) {
System.out.println("Error invoking service : "+e);
} catch(javax.xml.rpc.ServiceException e2) {
System.out.println("Error invoking service : "+e2);
} catch(java.rmi.RemoteException e3) {
System.out.println("Error invoking service : "+e3);
}
}
|
So what am I doing here? Well, first I create a QName
representing the port name. Axis doesn't provide a constructor
that creates a QName from a complete namespace so I have
to split the port name into the namespace and local part. I then create a
Service object supplying the URL of the WSDL implementation
and the QName representing the port name.
I then create a another QName object, this time representing
the service name. I now create a Call object from the
Service object. I set the operation this
Call object will invoke by passing in the port
QName and the operation name for it to invoke. And finally,
I actually invoke the operation passing in the Object[]
parameters created while parsing the WSDL. That's it! I now have the return value
as a Java Object and print it out.
I hope you've understood all the code I've gone through in this article. It's a bit heavy, but having said that, understanding how uddi4j,
wsdl4j, and Axis APIs work is important to effectively
programming customized Web services clients. The best thing now is to play with the code
and try to explore the information you can find from a UDDI registry or WSDL
document. If you're still a bit fuzzy about the structure of the UDDI elements or
the WSDL then try reading up on them. There are some excellent
resources on these
topics. Once again, if you have any questions regarding any of the content
presented in this article, please post a comment in the
Web Services Technical forum.
- Participate in the discussion forum.
- Download the source code for this article.
- The IBM Web Sphere Developer Kit for Web Services (WSDK) can be downloaded free of charge. It contains all of the software required to run the sample code supplied in this article. It is also a great way to get started developing Web services.
- The Apache Axis site contains documentation as well as free downloads.
- Get information on how to download wsdl4j.
- Check out the uddi4j site for information and downloads.
- Once you've played with the IBM Web Sphere Developer Kit for Web Services try the
IBM Web Services Toolkit which sits on top of the WSDK and contains cutting-edge Web services technology.
- Doug Tidwell has written an informative article, UDDI4J: Matchmaking for Web services on uddi4j (developerWorks, January 2001).
- Peter Brittenham, Francisco Curbera, David Ehnebuske, and Steve Graham collaborated on an excellent series of articles that explain how WSDL maps into a UDDI registry. There are three parts:
- Understanding WSDL in a UDDI registry, Part 1 (developerWorks, September 2001)
- Understanding WSDL in a UDDI registry, Part 2 (developerWorks, September 2001)
- Understanding WSDL in a UDDI registry, Part 3 (developerWorks, November 2001)
- For more information on WSDL try "Deploying Web services with WSDL: Part 1" by Bilal Siddiqui the CEO of WAP Monster (developerWorks, November 2001).
- If you're really into Web services this article, Web services interoperability between the WebSphere and .Net platforms is a must-read (developerWorks, August 2002) .
Damian Hagge works at IBM's Hursley Development Laboratories. Although he currently lives in the UK, he originated five hours counterclockwise from his present location just above the forty-ninth parallel (that's Canada for those whose geography isn't up to scratch). Aside from working in Web services and writing long articles on obscure subject matter, Damian is a veritable encyclopedia of useless facts; for instance, did you know that a hippopotamus has the greatest per-square-inch jaw pressure of any animal and is the most dangerous animal outside of water? Damian also does some normal stuff like power lifting (if you call that normal!). You can contact Damian Hagge at hagge@uk.ibm.com.