Sunday, June 14, 2009

Truly Dynamic Web Services on WebLogic using JAX-WS

In my last blog (a long time ago I know) I talked about the potential for using JAX-WS on WebLogic 10.3 to fulfil the 8 key values that I believe are important for Web Service based Messaging. I've been using JAX-WS in WebLogic a lot over the last half a year and I've been very impressed with its power. As promised, I'm now (finally) going to describe the results of my experiments, with JAX-WS Providers/Dispatchers specifically, to see if they can achieve 8 out of 8 on my scorecard. Does JAX-WS give me the power and freedom I desire when doing web services?

For the experiment, I have used a Customer based web service example, like I've used before. Using an Interface-First approach, I created a WSDL and associated XSD Schema for a fictitious "Change Customer Information" HTTP SOAP web service. Then I created the Java Web Service (JWS) implementing a JAX-WS Provider interface, rather than using a strongly-typed JAX-WS Service Endpoint Interface (SEI). To test out the integration between the WebLogic's JAX-WS runtime and WebLogic's Security sub-system, I included a WS-Policy in my WSDL to force service consumer applications to provide a WS-Security UserToken (username/password) in the SOAP header for authentication.

My code for the Provider is shown below:
@WebServiceProvider(
  serviceName="CustomerInfoChangeService",
  targetNamespace="testns:custinfochngsvc",
  portName="CustomerInfoChangeServiceSOAP",
  wsdlLocation="/WEB-INF/wsdls/CustomerInfoChangeService.wsdl"
)
@ServiceMode(value=Service.Mode.PAYLOAD)
public class CustomerInfoChangeService implements Provider<Source> {
  private final static String XSD_NMSPC = "testns:custinfo";
  @Resource
  private WebServiceContext context;
 
  public Source invoke(Source request) {  
    try {
      // Check some security stuff
      System.out.println("Service invoked with principal: " + 
                                       context.getUserPrincipal().getName());
      System.out.println("Is in Administrators group? " + 
                                     context.isUserInRole("Administrators"));
   
      // Process request
      Transformer transformer = TransformerFactory.newInstance().newTransformer();
      StreamResult sysOutStream = new StreamResult(System.out);
      System.out.println("Changing following customer info:");
      transformer.transform(request, sysOutStream);
      System.out.println();
   
      // INVOKE REAL XML PROCESSING AND CUSTOMER DB UPDATE LOGIC HERE
   
      // Construct response
      String responseText = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
        "<CustomerInfoChangeAcknowledgement xmlns=\"" + XSD_NMSPC + "\">" +
        "  <Ack>SUCCESS</Ack>" +
        "  <Comment>Successfully processed change for customer</Comment>" +
        "</CustomerInfoChangeAcknowledgement>";
      return new StreamSource(new StringReader(responseText));
    } catch (Exception e) {
      throw new WebServiceException("Error in Provider JAX-WS service", e);
    }
  }  
}

I bundled this Provider JWS into a plain WAR archive and deployed to WebLogic. I then created the service consumer code as a standalone Java application. The code for this client, which uses the JAX-WS Dispatch API to locate and invoke the remote Web Service, is shown here:
public class CustomerInfoChangeClient {
  private final static String WSDL_URL_SUFFIX = "?WSDL";
  private final static String WSDL_NMSP = "testns:custinfochngsvc";
  private final static String WSDL_SRVC_PORT = "CustomerInfoChangeServiceSOAP";
  private final static String WSDL_SRVC_NAME = "CustomerInfoChangeService";
  private final static String XSD_NMSP = "testns:custinfo";
  private final static String USERNAME = "weblogic";
  private final static String PASSWORD = "weblogic";
 
  public static void main(String[] args) {  
    if (args.length <= 0) {
      throw new IllegalArgumentException("Must provide endpoint URL arg");
    }

    try {
      new CustomerInfoChangeClient(args[0]);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public CustomerInfoChangeClient(String endpointURL) throws IOException, 
        SAXException, TransformerException, ParserConfigurationException {
    // Construct request
    String requestText = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
                         "<CustomerInfo xmlns=\"" + XSD_NMSP + "\">" +
                         "  <Id>1</Id>" +
                         "  <Address1>6</Address1>" +
                         "  <PostCode>AB1 2YZ</PostCode>" +
                         "</CustomerInfo>";
    Source request = new StreamSource(new StringReader(requestText));

    // Invoke service operation, adding appropriate credentials
    Service service = Service.create(new URL(endpointURL + WSDL_URL_SUFFIX),
                                      new QName(WSDL_NMSP, WSDL_SRVC_NAME));
    Dispatch<Source> dispatcher = service.createDispatch(new QName(
            WSDL_NMSP, WSDL_SRVC_PORT), Source.class, Service.Mode.PAYLOAD);
    Map<Object, String> rc = ((BindingProvider) dispatcher).
                                                     getRequestContext();
    List<CredentialProvider> credProviders = new 
                                          ArrayList<CredentialProvider>();
    credProviders.add(new ClientUNTCredentialProvider(USERNAME.getBytes(),
                                                    PASSWORD.getBytes()));
    rc.put(WSSecurityContext.CREDENTIAL_PROVIDER_LIST, credProviders);
    //rc.put(BindingProvider.USERNAME_PROPERTY, USERNAME);
    //rc.put(BindingProvider.PASSWORD_PROPERTY, PASSWORD);
    Source response = dispatcher.invoke(request);

    // Process response  
    Transformer transformer = TransformerFactory.newInstance().
                                                   newTransformer();
    StreamResult sysOutStream = new StreamResult(System.out);
    System.out.println("Change customer service result:");
    transformer.transform(response, sysOutStream);
    System.out.println();
  }
}

Notice that I have not used any generated XML-to-Java classes for the parameters and return values from within the client or in the service itself. Once compiled and run, the client application works completely as expected, and successfully authenticates with the remote web service using the specified username/password WS-Security credentials.

Here are my observations from the experiment:
  • In the simple example I just used StreamSource for processing the XML request and responses at both ends. However, I could have used other APIs such as DOMSource, JAXBSource, SAXSource, StAXSource from the Java 1.6 standard 'javax.xml.transform' packages instead (and even mixed and matched these for the request and the response).
  • I have used javax.xml.transform.Source for accessing the SOAP request and response messages, instead of javax.xml.soap.SOAPMessage. With Source I can only access the contents of the SOAP Body (refered to as the 'PAYLOAD'). With SOAPMessage, I can choose to access the XML elements of the SOAP Envelope (including SOAP headers) as well (refered to as the 'MESSAGE'). However using SOAPMessage restricts me to only being able to process XML using the W3C DOM API, therefore using Source appeals to me more. When using Source, if I need to access SOAP Headers, I'd probably just use JAX-WS Protocol Handlers anyway, to process these headers before or after my main Provider class is called.
  • I don't need to use WebLogic's WSDLC, JWSC or ClientGen Ant tasks because no "build-time" generation of Java classes (using JAXB) or stubs/skeletons is required. For my service, I could have still optionally used JWSC to generate a deployable WAR file with the appropriate web.xml deployment descriptor auto-generated, but I chose to create these artefacts myself, in a way that I can control, using simple Ant build.xml tasks.
  • To ensure that my JAX-WS service is detected properly by the WebLogic runtime, the key thing I needed to do was create a Servlet definition and mapping entry in my web.xml deployment descriptor for my JWS Provider class (even though a provider class does not actually implement javax.servlet.Servlet). During deployment, the WebLogic Web Service Container automatically detects that the web.xml declared servlet is infact a JWS class and automatically maps the URL specified to an internal WebLogic JAX-WS handling Servlet called "weblogic.wsee.jaxws.JAXWSWebAppServlet". As a result, my transport-protocol agnostic SOAP JWS class is now exposed over HTTP. My web.xml file includes the following:
   <servlet>
      <servlet-name>CustomerInfoChangeService</servlet-name>
      <servlet-class>test.service.CustomerInfoChangeService</servlet-class>
   </servlet>
   <servlet-mapping>
      <servlet-name>CustomerInfoChangeService</servlet-name>
      <url-pattern>/CustomerInfoChangeService</url-pattern>
   </servlet-mapping> 
[UPDATE 19-Jun-09: Actually after some further testing, I discovered that if I omit the servlet definition/mapping from my web.xml, the JWS Provider web-app still deploys and runs correctly. WebLogic automatically maps the Provider it discovers on deployment, to a URL which is the Provider class's name (without package name prefix and without .class suffix). This is in-line with the JavaEE 1.5 - Servlet 2.5 spec, where deployment descriptors are now optional. However, it may still be desirable to create a servlet definition/mapping in web.xml to be able to better control the URL of the service).]
  • I would have ideally liked to have included a @RolesAllowed() annotation in my Provider class to help declaratively restrict access to service operations based on role. However, the JAX-WS specification doesn't currently cater for this annotation and WebLogic's JAX-WS implementation does not process this annotation, if its included.
  • In addition to testing authentication using message-level security (eg. a WS-Security user token) I also tested authentication via transport-level security using a client provided HTTP Basic Authentication token (see commented out lines of code in the client app above). In both cases, authentication worked properly, and in my Provider class, when I call context.getUserPrincipal() the proper authenticated principal user object is returned.
  • However, when using context.isUserInRole("Administrators")) in my Provider class, "true" is only returned when using transport-level security, but not when using message-level security. This means that for message-level security, performing programmatic access control is currently limited to checking the user principal object only - the user's roles can't be queried. Here was the security role I defined in web.xml:
    <security-role>
       <role-name>Administrators</role-name>
    </security-role>
...and here is how I mapped the role to my WebLogic EmbeddedLdap 'Administrators' group in weblogic.xml:
    <security-role-assignment>
       <role-name>Administrators</role-name>
       <principal-name>Administrators</principal-name>
    </security-role-assignment>

So to wrap up, how does JAX-WS in WebLogic 10.3 stack up against my 8 criteria?

Well pretty well actually. I would say 7.75 out of 8. A full score on the first 7 criteria in the list and on the 8th (integration with the host container's Security Framework), just a 1/4 point dropped. This is due to a lack of full flexibility in defining declarative or programmatic access control for a service, when using message-level authentication.


Song for today: Festival by Sigur Rós

9 comments:

Anonymous said...

Hi, Paul Done,

I am interested in this paper ("Truly Dynamic Web Services on WebLogic using JAX-WS") you presented. Could you send me the source code So I can dig into more detail and play with, especially the provider part.

Thanks a lot.

Tina

Paul Done said...

Hi Tina. Sorry only just seen your post. Can you send me your email details to my gmail mail address, user: pkdone , so that I can send the sample project? Alternatively, check out my latest blog about the WebLogic book. The sample download projects for it include the Web Services chapter 9 examples, which covers much (but not all) of what is described in this blog post.

Ajay said...

This blog is really great.

I have IBM background (using Websphere products) and recently started using Weblogic for past few months. I was looking for some postings related to web service security and jax-ws support on Weblogic platform.

Would you be able to share any links/information related to WS-Policy? Strategies around how to apply web security - is it a good practice to do it from admin or should it be embedded inside WSDL, etc.

Also would you be able to send me this project described by this blog?

Thanks
AJ

Paul Done said...

--MOVING QUESTION FROM UNRELATED POST TO THIS POST--

Blogger charlie silver said...

Hi, Paul.

Excellent info you got here!

I came across this article of yours: http://pauldone.blogspot.com/2009/06/truly-dynamic-web-services-on-weblogic.html
when I was trying to find out information on the web about one error that I'm getting using the exact same implementation as your ws consumer, the only difference is that I'm consuming a SharePoint webservice, but as you point out in your article, this client is truly dinamic in a way that should allow you to call any web service in any platform.

Back to my problem, like I said, I wrote my client using the same approach as you to avoid the generation of the stubs/skeletons. So, if you don't mind, I would like to ask you a few questions:

1. I'm using the jax-ws 2.1 libraries. Did you use the same?

2. My client is able to call the sharepoint webservice locally, but is failing when I moved the client to my app server, that is a weblogic 10. Which version of weblogic did you use in your example?

3. Did you use any other library to build your client?

I hope you find the time to read my post. I really found your blog very informative.

Thanks,

Carlos

Paul Done said...

Hi Carlos

For the book chapter I provided some example 'dynamic' JAX-WS client code examples. Goto http://eu.wiley.com/WileyCDA/WileyTitle/productCd-0470484306.html , select Downloads tab and click the 'Code Downloads' link.
When you unzip this there are example projects for the book. Navigate to ch09 (the Web Services chapter examples) and look at README.txt. Directories example4 and example5 hanging off here show example client code using the Dispatcher interface. Can you have a look at this code to see if it helps you.

Note: WLS 10.0.1 included what was not really a production-ready JAX-WS impl (the product docs hint at this) - back then JAX-RPC was still recommended for implementation. Things changed in WLS 10.3.0 and onwards so I would recommend trying your code on that version first. If it works on 10.3.0 but not 10.0.1 then you may be stuck with just having to use JAX-RPC or a 3rd party toolkit. :(

Paul

charlie silver said...

Thanks a lot for your directions, Paul.

Carlos

Sriram said...

Hi Paul,

Thanks for this post.

I have a very similar implementation in my application however, I have frequently observed that dispatch.invoke results in a STUCK THREAD state. Please can help in diagonising what/where the issue is?

thanks,
Sriram

Unknown said...

Hi Paul,

I have a contact first web services application with WSDL and JAX-WS endpoints. All the endpoints have corresponding servlet-mapping entries in web.xml, similar to what you have described above.

The application EAR gets deployed in weblogic 11g, it is in active status, WSDL publishing also looks fine and I even get success response when I try any service operation. But web services are not getting displayed for my application in weblogic console. Can you please help me.

My issue seems exactly similar to as explained here (http://stackoverflow.com/questions/16019132/webservice-is-not-visible-in-weblogic-10-3)
Please help.

Binh Nguyen said...

Thanks, nice post