This Howto describes how to create a Web Service Endpoint and a J2EE client that uses this web service. The endpoint will provide a method for computing the compound interest from an initial investment, an interest rate (percent) and a time quantity.
The first task is to create a Java interface (aka Service Endpoint Interface, SEI). It must extend java.rmi.Remote.
The SEI will indicate the methods that will be used throughout the web service: that is, the web service interface (in the same context that an EJB has a Local or Remote interface) or, in effect, the Business interface.
package org.objectweb.interest; import java.rmi.Remote; import java.rmi.RemoteException; public interface Interest extends Remote { double getCompoundInterest(double principal, double interest, int terms) throws RemoteException; double getCompoundInterest(InterestBean ib) throws RemoteException; }
Once the SEI is created, an implementation for this interface must be provided. For the sake of simplicity, this Howto will expose the web service as a JAX-RPC Endpoint (i.e., a simple class, with no EJBs).
package org.objectweb.interest; import java.rmi.RemoteException; public class InterestImpl implements Interest { public double getCompoundInterest(double principal, double interest, int terms) throws RemoteException { double total = principal; for (int i = 0; i < terms; i++) { total = total * (1 + interest / 100.0); } return total - principal; } public double getCompoundInterest(InterestBean ib) throws RemoteException { // delegate to the real implementation :) return this.getCompoundInterest(ib.getPrincipal(), ib.getInterest(), ib.getTerms()); } }
The JavaBean that will be used in the SEI must be written. This JavaBean is not an interface; it is an actual Java Bean, with default constructor, setters, and getters:
package org.objectweb.interest; public class InterestBean { private double principal; private double interest; private int terms; public double getInterest() {return interest;} public void setInterest(double interest) {this.interest = interest;} public double getPrincipal() {return principal;} public void setPrincipal(double principal) {this.principal = principal;} public int getTerms() {return terms;} public void setTerms(int terms) {this.terms = terms;} }
The difficult part is bringing together all the XML/WSDL elements.
Since this example uses a JAX-RPC endpoint, the web service must be packaged inside a web application.
First, generate or write (whichever is preferred based on the level of familiarity with WSDL) the WSDL Definitions associated with the Interest interface. This WSDL will be updated and published by JOnAS, to be usable from one or more clients.
There is a helpful Axis ant task that takes an interface as parameter and generates a WSDL based on that interface:
<taskdef name="axis-java2wsdl" classname="org.apache.axis.tools.ant.wsdl.Java2WsdlAntTask"> <classpath refid="axis.classpath" /> </taskdef>
<axis-java2wsdl classname="org.objectweb.interest.Interest" location="http://dummy-url" serviceportname="InterestPort" namespace="urn:objectweb:demo:interest" typeMappingVersion="1.3" output="${xml.dir}/wsdl/Interest.wsdl" />
A simple mapping file is sufficent for this example:
<?xml version="1.0"?> <java-wsdl-mapping xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://www.ibm.com/webservices/xsd/j2ee_jaxrpc_mapping_1_1.xsd" version="1.1"> <package-mapping> <package-type>org.objectweb.interest</package-type> <namespaceURI>urn:objectweb:demo:interest</namespaceURI> </package-mapping> <java-xml-type-mapping> <java-type>org.objectweb.interest.InterestBean</java-type> <root-type-qname xmlns:ns="urn:objectweb:demo:interest">ns:InterestBean</root-type-qname> <qname-scope>complexType</qname-scope> </java-xml-type-mapping> </java-wsdl-mapping>
At this time, JOnAS must be told the name of the class that implements the actual service (InterestImpl). For a JAX-RPC endpoint, the JSR 921 (WS for J2EE 1.4 ) indicates that the implementation, fully-qualified classname goes into a servlet statement.
That servlet statement looks the same as a servlet definition:
<?xml version="1.0" encoding="ISO-8859-1"?> <web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4"> <display-name>Interest WebServices packaged in WebApp Sample</display-name> <servlet> <display-name>Interest web service</display-name> <servlet-name>InterestServlet</servlet-name> <servlet-class>org.objectweb.interest.InterestImpl</servlet-class> </servlet> </web-app>
The core webservice descriptor, webservices.xml, should now be placed in WEB-INF for a JAX-RPC endpoint and in META-INF for an exposed SessionBean.
This is the file in which JOnAS is told how to link a wsdl:port and its implementation bean (service-impl-bean).
Start by creating a webservice-description element, giving it a unique name (Interest Webservice). Then add the wsdl-file tag with the path (inside the webapp) to the generated WSDL. Do the same thing for jaxrpc-mapping-file.
Notice that the WSDL file MUST be located in WEB-INF/wsdl (or META-INF/wsdl for SessionBean).
Once this is completed, it is necessary to describe how the WSDL will be mapped into a JOnAS component. This is done via the port-component; it maps a wsdl-port (give its QName, {urn:objectweb:demo:interest}InterestPort, which is the port name identified in the WSDL) to the SEI (service-endpoint-interface). Then a link (servlet-link or ejb-link) must be added for the component that actually performs the job. In this case, the job is done by the "servlet" named InterestServlet.
<?xml version="1.0"?> <webservices xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://www.ibm.com/webservices/xsd/j2ee_web_services_1_1.xsd" version="1.1"> <display-name>Interest Sample</display-name> <webservice-description> <webservice-description-name>Interest Webservice</webservice-description-name> <wsdl-file>WEB-INF/wsdl/Interest.wsdl</wsdl-file> <jaxrpc-mapping-file>WEB-INF/InterestMapping.xml</jaxrpc-mapping-file> <port-component> <port-component-name>InterestPC</port-component-name> <wsdl-port xmlns:ns="urn:objectweb:demo:interest">ns:InterestPort</wsdl-port> <service-endpoint-interface>org.objectweb.interest.Interest</service-endpoint-interface> <service-impl-bean> <servlet-link>InterestServlet</servlet-link> </service-impl-bean> </port-component> </webservice-description> </webservices>
Important: The URL where the web service will be available depends on the values sets in this file:
A URL will look like the following:
Now it is time to package the webapp.
As usual, the compiled classes go into WEB-INF/classes; web.xml is located directly in WEB-INF.
News that comes from webservices.xml will also go into WEB-INF, along with the JAX-RPC mapping file. And, as mentioned above, the WSDL goes into WEB-INF/wsdl.
Package all of these elements inside a war.
Webapp content:
interest.war | +-- WEB-INF | +-- web.xml +-- webservices.xml +-- InterestMapping.xml | +-- classes/ | | | +-- org.objectweb.interest.Interest* | +-- wsdl/ | +-- Interest.wsdl
Now that a generic webapp has been created, all the server-specific files can be generated. For those familiar with EJB, there is a GenIC-like step for web services; all J2EE modules that deal with web services (in a standard way) must be prepared before the installation into JOnAS.
The tool that handles this for web services is called WsGen. WsGen is bundled with an Ant Task so that it can be easily used by developers. It is only necessary to specify 1) the srcdir attributes to tell the task where the archive(s) are located, and 2) the destdir attribute to indicate where the output files should be placed.
Note that WsGen will place a modified webapp inside ${destdir}/webapps. It also does this for other archive types (ejbjars, clients, apps)
<wsgen srcdir="${temp.dir}" destdir="${dist.dir}" <include name="webapps/interest.war" /> </wsgen>
Everything on the server side is now complete.
This last step is to install the newly-generated webapps into JOnAS:
>$ jonas admin -a interest.war
Make sure that a JOnAS instance is running before typing this command.
The WSDL for the deployed web service should be available at the following location:
http://localhost:9000/interest/Interest%20Webservice/InterestPort?JWSDL
It is a good idea to verify this.
If everything is working fine, go to the next step, which is the client programming.
In this section, a web-service client that uses the Interest endpoint will be created.
In a real-world client, it is the developer's responsibility to choose an appropriate web service endpoint. Once the web service has been selected, the developer must retrieve the technical description of the service, which is the WSDL.
In this example, the WSDL should be at the following location (see [#Install]]):
http://localhost:9000/interest/Interest%20Webservice/InterestPort?JWSDL
Use a Copy and Paste command to copy this URL into a browser. Save the WSDL on a hard disk. Note that this WSDL supplies the right URL in its soap:address[@location].
In this coding section one method of writing service clients will be employed and a few interfaces will be written.
The service endpoint interface that will be used on the client side is exactly the same as the one that has already been written for the server side. Thus, in this particular example, simply copy the org.objectweb.interest.Interest class.
However, in a real-life client (with only the WSDL and no classes present yet), it would be necessary to either:
Important: When creating/generating SEI, the parameters for the operations must be identified. If the method uses a complex type, this type must be provided. The J2EE Container (i.e., JOnAS) will not generate it.
A simple rule to follow is: The SEI MUST be compilable, thus all dependencies must be available in the classpath.
The service interface can be compared to the Home/LocalHome interface of an EJB; it provides access to the SEI (that contains all the 'business' methods).
In most cases, just using the default service interface (javax.xml.rpc.Service) should be sufficent. However, a service interface (that extends javax.xml.rpc.Service) can be written with meaningful SEI accessors. In short, with the Service interface, the SEI is obtained via Service.getPort(...). However, it may be preferable to use MyService.getInterest().
Since this example is designed to be easy, a Service Interface will not be written; javax.xml.rpc.Service will be used.
This section describes how to code the actual web-service client: the class that will make things useful with the web service.
A simple servlet will be developed that will read the parameters needed to call the Interest, get the port, and finally execute the request.
private void processRequest(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException { // get the parameters String principal = req.getParameter("principal"); String interest = req.getParameter("interest"); String terms = req.getParameter("terms"); // parse them double pDouble = Double.valueOf(principal).doubleValue(); double iDouble = Double.valueOf(interest).doubleValue(); int tInteger = Integer.valueOf(terms).intValue(); // retrieve InitialContext InitialContext ictx = null; try { ictx = new InitialContext(); } catch (NamingException e) { ServletException se = new ServletException("Cannot create InitialContext", e); throw se; } // perform the lookup on the Service instance Service service = null; String lookup = "java:comp/env/service/interest"; try { service = (Service) ictx.lookup(lookup); } catch (NamingException ne) { ServletException se = new ServletException("Cannot lookup : " + lookup, ne); throw se; } // get the Port based on the Interface class Interest port = null; try { port = (Interest) service.getPort(Interest.class); } catch (ServiceException svce) { ServletException se = new ServletException("Cannot get Port from " + Interest.class.getName(), svce); throw se; } // perform the ws operation double result = port.getCompoundInterest(pDouble, iDouble, tInteger); // perform the ws operation with Complex Type InterestBean ib = new InterestBean(); ib.setPrincipal(pDouble); ib.setInterest(iDouble); ib.setTerms(tInteger); double result2 = port.getCompoundInterest(ib); // print the result res.getWriter().print(header); res.getWriter().print("port.getCompoundInterest(" + principal + ", " + interest + ", " + terms + ")=" + result); res.getWriter().print("port.getCompoundInterest(" + ib + ")=" + result2); res.getWriter().print(footer); }
This servlet provides much useful information:
It is also a good idea to write a simple HTML page that will pass values to the servlet. However, that is not covered this Howto.
With the servlet coded, the next step is to write the XML descriptors.
The following servlet is the equivalent of the env-entry-ref for web services: 'service-ref'.
<?xml version="1.0" encoding="ISO-8859-1"?> <web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4"> <display-name>WebApplication using Distant WebServices</display-name> <servlet> <servlet-name>InterestClientServlet</servlet-name> <servlet-class>org.objectweb.interest.InterestClientServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>InterestClientServlet</servlet-name> <url-pattern>/interest.do</url-pattern> </servlet-mapping> <welcome-file-list id="WelcomeFileList"> <welcome-file>index.html</welcome-file> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <service-ref> <service-ref-name>service/interest</service-ref-name> <service-interface>javax.xml.rpc.Service</service-interface> <wsdl-file>WEB-INF/wsdl/InterestPort.wsdl</wsdl-file> <jaxrpc-mapping-file>WEB-INF/InterestMapping.xml</jaxrpc-mapping-file> <port-component-ref> <service-endpoint-interface>org.objectweb.interest.Interest</service-endpoint-interface> </port-component-ref> </service-ref> </web-app>
The servlet can be declared as usual (using servlet), along with its servlet-mapping.
service-ref must be added explicitly for the web service reference:
There is also a jonas-web.xml:
<?xml version="1.0" encoding="UTF-8"?> <jonas-web-app xmlns="http://www.objectweb.org/jonas/ns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.objectweb.org/jonas/ns http://www.objectweb.org/jonas/ns/jonas-web-app_4_1_2.xsd" > <jonas-service-ref> <service-ref-name>service/interest</service-ref-name> <jonas-port-component-ref> <service-endpoint-interface>org.objectweb.interest.Interest</service-endpoint-interface> <wsdl-port xmlns:ns="urn:objectweb:demo:interest">ns:InterestPort</wsdl-port> </jonas-port-component-ref> </jonas-service-ref> </jonas-web-app>
It is only used to tell JOnAS which wsdl:port should be returned when requesting a Port using the Class as parameter (service.getPort(Class)).
Because such a file has already been written for the endpoint sample, it can be copied directly into the client.
Remember that, if the client is a real-world client with only WSDL, a mapping file MUST be written. Unfortunately, there is no free tool that can handle all the complexities of the mapping file.
The Web services and J2EE descriptors have now been completed.
The goal for this section is to build the webapp. This is all fairly standard.
Content of the webapp client:
interest-client.war | +-- (all static stuff here : index.html, ...) | +-- WEB-INF | +-- web.xml +-- jonas-web.xml +-- InterestMapping.xml | +-- classes/ | | | +-- org.objectweb.interest.Interest* | +-- wsdl/ | +-- Interest.wsdl
Run WsGen with the webapp to generate all the required elements (including the endpoint) for running the component inside JOnAS.
<wsgen srcdir="${temp.dir}" destdir="${dist.dir}" <include name="webapps/interest-client.war" /> </wsgen>
The final task is to install the client webapp inside JOnAS:
>$ jonas admin -a interest-client.war
Make sure that a JOnAS instance is running before typing this command.
Go to: http://localhost:9000/interest-client and view the results.