Friday, December 19, 2008

Controlling what service a proxy uses at deployment time

This blog represents the non-policy part of my demo at UKOUG'08 this year. In that scenario we had a simple loan application that made use of a "mocked" version of a credit rating service during developement and a more real service for production. (Mocked up using the HTTP Analyzer in this case) This following screen grab gives you an idea of the project structure:

If we ignore the deploy directory for now we can take a look at the code in LoanApprover. The key points to notice here is the @WebServiceRef which injects the endpoint from the web.xml and the callout method to provision security. The important thing here is that the new WSDL might have different security policy; but it is possible to write the code fairly generically to take this into account because of the way that the web logic client auto-configures. In this case the mock service had no security and the production service had WS-Security with plain username/password.

@WebService 
public class LoansApprover {

    /**
     * Credit rating service injected from web.xml
     **/
    @WebServiceRef(name = "CreditRatingService")
    CreditRating creditRating;
    
    /**
     * @return Loan application with approval code if
     *   approved.
     */

    public LoanApprovalReponse approveLoan(LoanApplication la)  {

        LoanApprovalReponse approvalReponse = new LoanApprovalReponse();

        // Provision credentials
        //
        
        CredentialProvisioner.provisionPort(creditRating);

        // Start looking up credit rating 
        Response ratingReponse = creditRating.lookupRatingAsync(
           la.getSsid());
        
        // Retrieve any customer records
        // ...
        // Process Credit rating
        
        try {
            int creditRating = ratingReponse.get();
            if (creditRating > 30) {
                approvalReponse.setApprovalCode(
                    UUID.randomUUID().toString());
            }

        } catch (Exception e) {
            e.printStackTrace(); // Do nothing
        }
        
        return approvalReponse;
    }
}

So the @WebServiceRef uses a name resource, defined some where in JNDI. In this case it is in web.xml for an EJB it would be in ejb-jar.xml. Lets take a quick look, note that because we specify the service class name the deployer runtime code will work out to inject the port to save you a little bit of typing.

<?xml version = '1.0' encoding = 'windows-1252'?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5" xmlns="http://java.sun.com/xml/ns/javaee">

    ...    
    
    <service-ref>
        <service-ref-name>CreditRatingService</service-ref-name>
        <service-interface>com.somecreditrating.xmlns.rating.CreditRating_Service</service-interface>
    </service-ref> 
</web-app>

So in development the service class has all the hard coded references to the mock service deployed on localhost. For production we are going to change the WSDL that the proxy loads.

If you look at the project structure you will notice that there is an production.ear file in a rather novel directory structure. You will notice that the ear lives under the "app" sub dir and there is a "plan" directory. This structure will be created for you if you do something like change a policy on a deployed web service; but we need this all in place from the start. (Take a look at the weblogic documentation for more information on this structure)

Now you have to create a plan.xml, see this information on how to create one from the command line, but in the best Blue Peter tradition here is one I created earlier. The key parts are the variable definition and the assignment later on that uses xpath to create a new wsdl reference. Be carefull with the xpath expression as a space in the wrong place can cause the expression not to work. When deployed the proxy created for @WebServiceRef will read from this WSDL not the hard coded one.

<?xml version='1.0' encoding='UTF-8'?>
<deployment-plan xmlns="http://www.bea.com/ns/weblogic/deployment-plan" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.bea.com/ns/weblogic/deployment-plan http://www.bea.com/ns/weblogic/deployment-plan/1.0/deployment-plan.xsd" global-variables="false">
  <application-name>production</application-name>
  <variable-definition>
    <variable>
      <name>CreditRatingService</name>
      <value>http://www.somecreditrating.com/xmlns/rating?WSDL</value>
    </variable>
  </variable-definition> 
  <module-override>
    <module-name>production.ear</module-name>
    <module-type>ear</module-type>
    <module-descriptor external="false">
      <root-element>weblogic-application</root-element>
      <uri>META-INF/weblogic-application.xml</uri>
    </module-descriptor>
    <module-descriptor external="false">
      <root-element>application</root-element>
      <uri>META-INF/application.xml</uri>
    </module-descriptor>
    <module-descriptor external="true">
      <root-element>wldf-resource</root-element>
      <uri>META-INF/weblogic-diagnostics.xml</uri>
    </module-descriptor>
  </module-override>
  <module-override>
    <module-name>LoanApplication-LoanApprover-context-root.war</module-name>
    <module-type>war</module-type>
    <module-descriptor external="false">
      <root-element>weblogic-web-app</root-element>
      <uri>WEB-INF/weblogic.xml</uri>
    </module-descriptor>
    <module-descriptor external="false">
      <root-element>web-app</root-element>
      <uri>WEB-INF/web.xml</uri>
      <variable-assignment>
        <name>CreditRatingService</name>
        <xpath>/web-app/service-ref/[service-ref-name="CreditRatingService"]/wsdl-file</xpath>
        <operation>add</operation>
      </variable-assignment> 
    </module-descriptor>
    <module-descriptor external="true">
      <root-element>weblogic-webservices</root-element>
      <uri>WEB-INF/weblogic-webservices.xml</uri>
    </module-descriptor>
    <module-descriptor external="false">
      <root-element>webservices</root-element>
      <uri>WEB-INF/webservices.xml</uri>
    </module-descriptor>
    <module-descriptor external="true">
      <root-element>webservice-policy-ref</root-element>
      <uri>WEB-INF/weblogic-webservices-policy.xml</uri>
    </module-descriptor>
  </module-override>
  <config-root>D:\prom-demo\jdeveloper\mywork\LoanApplication\deploy\production\.\plan</config-root>
</deployment-plan>

Now the deployment plan dir also allows you to add files to the classpath. In this case we have a simple properties file that provides a username and password to be later configured as a credential provider. (Do consider storing your password somewhere more safe) For completeness lets just check out the code that provisions the proxy with the username and password token. Nothing very special there.

private static Properties passwordStore;

public static void provisionPort(Object port) {
    assert port instanceof BindingProvider;
    
    BindingProvider bp = (BindingProvider)port;
    
    // Get class and find the port name, for the moment 
    // lets just cheat
    //
    
    Class proxyPort = port.getClass();
    Class ifClass = proxyPort.getInterfaces()[0];
    String portName = ifClass.getName();
    
    
    // Do some clever lookup in a keystore
    //
    
    Pair usernamepassword =
        getCredentials(portName); 
    if (usernamepassword!=null) {

        ClientUNTCredentialProvider credentialProvider =
            new ClientUNTCredentialProvider(usernamepassword.getLeft().getBytes(),
                                            usernamepassword.getRight().getBytes());
        
        bp.getRequestContext().put(
            WSSecurityContext.CREDENTIAL_PROVIDER_LIST,
            Collections.singletonList(credentialProvider));

    }
    
    
}

private static Pair getCredentials(String portName) {

    if (passwordStore==null)
    {
        passwordStore = new Properties();
        URL password_properties = CredentialProvisioner.class.getResource(
                                      "password.properties");
        if (password_properties!=null) {
            try {
                passwordStore.load(password_properties.openStream());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    // Get the packed version of the string
    //
    
    String packedVersion = passwordStore.getProperty(portName);
    if (packedVersion!=null) {
    
        String splitVersion[] = packedVersion.split("::");
        assert splitVersion.length == 2;
        
        return new Pair(splitVersion[0],splitVersion[1]);
    }
    else {
        return null;
    }

}

Unfortunatelly you cannot deploy this special directory structure from within JDeveloper, so you need to go into the weblogic console, Deployments->Install, but as you can see from the screen grab it will recognize the app/plan combo as a directory you can deploy:

And that is that, you application will deploy, read the new WSDL and start to use the external service.

7 comments:

Unknown said...

Hi Gerard,
Can this approach be used for Web Services Proxy? I did not seem to find any @WebServiceRef information in web.xml. Secondly, the variables defined in plan.xml are name-value-pair format, how does the injection work? In your example, there is "CreditRatingService" variable in the deployment plan, how does it correctly inject the value in "service-interface" of CreditRatingService "service-ref-name"?
Thank you in advance!

Gerard Davison said...

Jimmy,

You have to put the entries in the web.xml yourself I am afraid. As to the plan, there are name/value pairs at the top that are used later on, look near the end of the plan and you should find a xpath selecting the right entry.

Gerard

Edwin Biemond said...

Hi Gerard

WebServiceRef injection is not working for me , I am using jdev11g and the internal wls

I tried to use WebServiceRef with wsdl url and also tried it with the deployment plan.

I need to use proxy client constructor else I get a nullpointer exception.

I know standalone the injection is not working but I would expect it is working in the wls container

thanks Edwin

Gerard Davison said...

Edwin,

Could you please give me an idea of what you have tried? What does your @WebServiceRefe look like? What does your web.xml look like?

Gerard

Edwin Biemond said...

Hi,

I tried to use it in a jdeveloper jax-ws proxy client. I saw some documents of ibm that in this case the injection doesn't work and I need to the constructor to pass on the wsdl url.

It only works in a webservice annotation

Thanks Edwin

BradW said...

Hi Gerard. I am running into the same issue as Edwin. I'm trying to use this for my web service client (internal class from the client class) and I am getting a NPE. Is there a good solution for what I'm trying to do?

Thanks,


BradW

Gerard Davison said...

The most likely reason for failure is a missing service ref in the web.xml, without any more information it would be hard to comment.