6.7 Client and Server Context Handling

   

6.7 Client and Server Context Handling

All of the JAX-RPC examples that you have seen so far have treated the client application and the service implementation as independent entities that communicate only by passing information as method arguments and return values. In reality, however, it is often useful for the server to have access to additional context information that is supplied by the environment in which it operates or is propagated from the client on each method call. This section looks at the various types of context information that the JAX-RPC runtime makes available to web service implementations .

The web service implementation classes discussed so far have been self-contained and have confined themselves to implementing the methods of the service endpoint interface. Resources that might be required, such as the list of books known to a book service or the cover images for those books, have been bundled into the WAR file along with the implementation class, and accessed at runtime using the Class getResource( ) and getResourceAsStream( ) methods. This technique is acceptable when all of the resources and configuration information for a web service are known when the service is packaged, but it does not allow for configuration to be performed at deployment time. Web containers typically provide some mechanism that allows configuration of this type to be performed, and the servlet environment includes APIs that allow access to this information at runtime. However, there is a problem. How does the service implementation class get access to the environment of its hosting servlet? The JAX-RPC server-side API includes an interface ( javax.xml.rpc.server.ServiceLifecycle ) that a service class can implement that makes this possible.

As discussed in Chapter 2, J2EE Version 1.4 allows a web service to be hosted in a servlet environment or implemented as a stateless session bean. The discussion in this section relates only to the former case. A web service implemented as a session bean can use the lifecycle methods of the bean itself to perform initialization and cleanup, while runtime context information is provided by the SessionContext object passed to its setSessionContext( ) method. Therefore, the ServiceLifecycle interface is not applicable to a web service implemented as a session bean.

6.7.1 The ServiceLifecycle Interface

The definition of the ServiceLifecycle interface is shown in Example 6-30.

Example 6-30. The ServiceLifecycle interface
 public interface ServiceLifecylce {
    public void init(Object context) throws ServiceException;
    public void destroy(  );
} 

When JAXRPCServlet (or an implementation-specific equivalent) receives requests that require the invocation of a method of a web service, it creates one or more instances of the servant class that provides that service. If (and only if) the service class implements the ServiceLifecylce interface, then following execution of its constructor and before any of the methods that implement the service endpoint interface are invoked, JAXRPCServlet calls its init( ) method. A servant class normally uses the init( ) method to perform resource allocation, and may throw a ServiceException if it encounters any errors that would prevent it from providing its service. If JAXRPCServlet can determine that it no longer requires an instance of a servant class, then when its hosting web container is being closed down, it typically calls the servant class's destroy( ) method to allow it to release resources. JAX-RPC implementations that create a pool of instances of a servant class call the destroy( ) method of an instance when removing it from the pool, following which no further web service method calls will be delegated to it.

When the init( ) method of the servant class is called, it is passed an instance of an object that provides runtime context information. In order to allow web services to be hosted in different environments, the init( ) method signature simply declares this argument to be of type Object . However, for the servlet environment, the actual runtime type of this object is javax.xml.rpc.server.ServletEndpointContext . This object remains valid throughout the lifetime of the service instance (that is, until the destroy( ) method is completed), and a reference to it may therefore be stored for use within the methods that implement the service endpoint interface.

6.7.2 The ServletEndpointContext Interface

The ServletEndpointContext object provides access to the runtime environment in which a servant class hosted by a servlet is executing. The methods defined by this interface are shown in Example 6-31.

Example 6-31. The ServletEndpointContext interface
 public interface ServletEndpointContext {
    public ServletContext getServletContext(  );
    public Principal getUserPrincipal(  );
    public HttpSession getHttpSession(  );
    public MessageContext getMessageContext(  );
} 

These methods are typically used as follows :

  • The getServletContext( ) method retrieves the ServletContext object for the web application hosting the web service implementation. This is the only attribute of the ServletEndpointContext that is valid during the execution of the init( ) method. Among other things, the ServletContext gives you access to the servlet's initialization parameters, which are set when the web service is packaged and may be overridden at deployment time. You can therefore use the web.xml file for the web application in which the service is deployed to supply configuration information intended for the web service itself. An example of this is shown later.

  • The getUserPrincipal( ) method, which can be called only from within a method implementing the service endpoint interface, returns a java.security.Principal object for the user that invoked the endpoint interface method. The value returned from this method is null unless the hosting servlet is configured so that the web container performs authentication for the URL used to access the service. When authentication is enabled, the client application is required to supply authentication information. Requests that fail to do so, or that supply invalid credentials, will be rejected by the web container before the web service method is called. See Section 6.7.6 later in this chapter for an example of the use of web service authentication.

  • The getHttpSession( ) method returns the javax.servlet.http.HttpSession object for the HTTP session within which the web service method is invoked (if there is one). If the client application does not explicitly enable the use of HTTP sessions, then this method returns null . See Section 6.7.5 for an example of the use of an HTTP session.

  • The getMessageContext( ) method returns a MessageContext object that can be used to acccess the SOAP message that caused the currently executing service endpoint interface method to be called. The same MessageContext object is shared by any message handlers configured for the endpoint; therefore, it can also be used to store state information that needs to be shared between message handlers or between message handlers and the web service implementation itself. See Section 6.8 later in this chapter for further information on the MessageContext object.

In addition to the state available from the ServletEndpointContext , a web service implementation class can access information stored in the JNDI environment of its hosting servlet.

6.7.3 Multithreaded Access to the ServletEndpointContext Object

Even though a single instance of the ServletEndpointContext interface is passed to a servant's init( ) method, the values of each of the attributes of this object, apart from the ServletContext , depend on the context of a specific web service method and are therefore valid only when that method is being executed. However, as discussed in Chapter 2, a single servant instance may be used to service any number of web service method invocations, which may or may not be serialized relative to each other. In other words, it is possible that there may be more than one thread simultaneously executing the servant's methods at any given time.

This situation might seem to cause a problem with the use of the ServletEndpointContext object, since it needs to return the attributes applicable to one client application in one thread and those for a different application (and probably a different user) in another thread. Fortunately, there is no conflict between simultaneous uses of the ServletEndpointContext object in different threads, since the request-specific information that it makes available is held in thread-local variables . Each thread, therefore, has its own private copy of the state relating to the client application request that it is handling, which is returned by the methods of the ServletEndpointContext when invoked from that thread.

6.7.4 An Example Web Service Using ServletEndpointContext

To illustrate the use of the ServiceLifecycle and ServletEndpointContext interfaces, we'll use an extended version of the SmallBookService that was shown earlier in this chapter. The definition of the service interface for this example is shown in Example 6-32.

Example 6-32. The ContextBookQuery service endpoint interface
 public interface ContextBookQuery extends Remote {
    public abstract void setUpperCase(boolean cond) throws RemoteException;
    public abstract boolean isUpperCase(  ) throws RemoteException;
    public abstract int getBookCount(  ) throws RemoteException;
    public abstract String getBookTitle(int index) throws RemoteException;
    public abstract void getBookAuthor(String title, StringHolder author) 
        throws ContextBookServiceException, RemoteException;
    public abstract void log(String value) throws RemoteException;
} 

The only difference between this interface and the SmallBookQuery interface is the addition of the setUpperCase( ) and isUpperCase( ) methods, which are included for the sake of demonstrating the use of HTTP sessions in the next section. From the point of view of this section, the most interesting aspect of this example is the servant implementation class, extracts from which are shown in Example 6-33.

Example 6-33. Extract from the implementation class for the ContextBookQuery interface
 public class ContextBookServiceServant implements ContextBookQuery, ServiceLifecycle {
    
    // ServletEndpointContext object
    private ServletEndpointContext endpointContext;
    
    // Name of an authorized user.
    private String userName;
    
    // Records whether book names should be sorted
    private boolean sorted;
    
    /* -- Implementation of the ServiceLifeCycle interface -- */
    public void init(Object context) throws ServiceException {
        endpointContext = (ServletEndpointContext)context;
        
        // Get the authorized user name from the init parameters
        ServletContext servletContext = endpointContext.getServletContext(  );
        userName = servletContext.getInitParameter("UserName");
        
        // Get alphabetic sorting flag from JNDI
        try {
            InitialContext namingCtx = new InitialContext(  );
            Object value = namingCtx.lookup("java:comp/env/sorted");
            sorted = value instanceof Boolean ? ((Boolean)value).booleanValue(  ) 
                                              : false;
            ContextBookServiceServantData.setSorted(sorted);
        } catch (NamingException ex) {
            servletContext.log("Exception while accessing naming context", ex);
        }
    }

    /** 
     * Called when the service instance is no longer required.
     */
    public void destroy(  ) {
        // Nothing to do 
    }
    
    // Service endpoint interface implementation methods not shown
} 

Since this class implements the ServiceLifecycle interface, it is obliged to provide the init( ) and destroy( ) methods, although in the case of the latter, there is nothing to do for this particular service. The init( ) method begins by casting the object that is passed to the type ServletEndpointContext , and storing it for later use in the web service implementation methods. The remaining code in this method illustrates two ways for the servant class to get configuration information:

  • Servlet initialization parameters can be obtained by calling the getInitParameter( ) method of ServletContext . The ServletContext for the hosting servlet is obtained by calling the g etServletContext( ) method of ServletEndpointContext . In this case, the init( ) method gets the value of an initialization parameter called UserName , which it stores for later use. This value is used in the second part of Example 6-33, and is described later in Section 6.7.6.

  • The values associated with entries in the hosting servlet's JNDI namespace can be obtained by creating an InitialContext object and then using the lookup( ) method with an appropriate key. Here, the value of a Boolean setting held under the key java:comp/env/sorted is retrieved. Service implementation classes are permitted to access the JNDI context in their web service implementation methods, as is init( ) .

The initialization parameter and the value of the sorted entry in the JNDI environment can be set by including appropriate tags in the web.xml file that is included in the portable WAR. The web.xml file for this example, with the tags needed to declare the parameters used in this example highlighted, is shown in Example 6-34. [10]

[10] The web.xm l file also contains a security constraint that ensures that only users in the JWSGroup role can access this web service. You'll see how a JAX-RPC client identifies itself to the web container in order to satisfy this constraint in Section 6.7.6, later in this chapter.

Example 6-34. The web.xml file for the ContextBookQuery web service
 <!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/j2ee/dtds/web-app_2_3.dtd">

<web-app>
    <display-name>Context-handling JAX-RPC Book Service</display-name>
    <description>Context-handling Book Service Web Application using JAX-RPC
        </description>  <context-param>
        <param-name>UserName</param-name>
        <param-value>JWSUserName</param-value>
    </context-param>  <security-constraint>
        <web-resource-collection>
            <web-resource-name>ContextBookService</web-resource-name>
            <url-pattern>/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>JWSGroup</role-name>
        </auth-constraint>
    </security-constraint>
    <login-config>
        <auth-method>BASIC</auth-method>
        <realm-name>Context Book Service</realm-name>
    </login-config>  <env-entry>
        <env-entry-name>sorted</env-entry-name>
        <env-entry-value>true</env-entry-value>
        <env-entry-type>java.lang.Boolean</env-entry-type>
    </env-entry>
</web-app>  

As well as providing access to initialization parameters, having a reference to the ServletContext allows the servant class to make entries in the web container's log file. The log( ) method in the service endpoint interface exploits this fact to improve on the original implementation described in Section 6.3.4 earlier in this chapter, which wrote the logging information to System.out :

 public void log(String value) {
    if (checkAccess(  )) {  // checkAccess(  ) is described later in this section
        endpointContext.getServletContext(  ).log(new Date(  ) + ": " + value);
    }
} 

To run this example, make chapter6\contextbookservice your working directory and type the command:

 ant deploy 

This command compiles, packages, and deploys the service implementation. The client application for this example calls the getBookCount( ) method of the service endpoint interface to get the number of books, then loops to get the author and title of each, printing the results in the command window. You can run this client using the commands:

 ant compile-client
ant run-client 

When you run these commands, you'll notice that the book titles appear in alphabetical order. This happens because the ContextBookServiceServantData class, which provides the book data to the servant class for this example, uses the value of the Boolean variable obtained from the JNDI value java:comp/env/sorted , as shown in Example 6-33, to determine whether to return sorted data. As you can see from the web.xml file in Example 6-34, this variable is set to true and therefore the book titles are sorted.

6.7.5 Session Management

For reasons discussed in Section 2.2.4.1, servant classes cannot store state information relating to individual client applications. As a result, the methods in the service endpoint interface are almost always self-describing in the sense that all required inputs to the operation are provided as arguments. However, a web service that is hosted by a servlet can store client- related information if it makes use of the HTTP session support provided by the web container.

The servant class gets access to the session information by calling the getHttpSession( ) method of the ServletEndpointContext object passed to its init( ) method, and then uses the HttpSession setAttribute( ) method to store values that can be retrieved in subsequent invocations of the endpoint interface methods. For an example of how this might be used, consider the setUpperCase( ) and isUpperCase( ) methods in the ContextBookQuery interface shown in Example 6-32. The intent of the setUpperCase( ) method is to allow the client to specify whether the book titles returned by the getBookTitle( ) method should be all in their natural case or in uppercase. A client should be able to invoke this method once and then be able to get the title of any book without having to respecify the required case setting with each method call. The isUpperCase( ) method should return the value set by the most recent invocation of setUpperCase( ) by the same client, or else return a default value if the client has never called it. The state set by the setUpperCase( ) method needs to be stored on a per-client basis.

In order to provide this functionality, the servant class needs to keep the value set by the setUpperCase( ) method in the HTTP session associated with the client that invokes it, from which it can be retrieved later when it is required. The implementation of the setUpperCase( ) and isUpperCase( ) methods is shown in Example 6-35.

Example 6-35. Using an HTTP session to store client-related state
 public void setUpperCase(boolean cond) {
    HttpSession session = endpointContext.getHttpSession(  );
    if (session != null) {
        session.setAttribute(UPPER_CASE, cond ? Boolean.TRUE : Boolean.FALSE);            
    }
}

public boolean isUpperCase(  ) {
    HttpSession session = endpointContext.getHttpSession(  );
    if (session != null) {
        Boolean upperCase = (Boolean)session.getAttribute(UPPER_CASE); 
        return upperCase == null ? false : upperCase.booleanValue(  );
    }      

    // No session - upper case mode not allowed
    return false;
} 

As you can see, both methods check whether the value returned by getHttpSession( ) is null before attempting to access or store a value. This step is necessary because the client application might not have enabled the use of HTTP sessions or the web container might not support them. In order to enable HTTP sessions, the client must explicitly set a property on the JAX-RPC client stub:

 // Get the endpoint interface
ContextBookService_Impl service = new ContextBookService_Impl(  );
ContextBookQuery bookQuery = service.getContextBookQueryPort(  );
Stub stub = (Stub)bookQuery;
stub._setProperty(Stub.ENDPOINT_ADDRESS_PROPERTY, args[0]);  // Enable session maintenance so that the setUpperCase(  ) method works  .  stub._setProperty(Stub.SESSION_MAINTAIN_PROPERTY, Boolean.TRUE);  

The same technique is required for a dynamic proxy Stub obtained using one of the Service getPort( ) methods. If you are using the dynamic invocation interface, then the Call.SESSION_MAINTAIN_PROPERTY of the Call object should be set to Boolean.TRUE :

 Call call = service.createCall(...);
call.setProperty(Call.SESSION_MAINTAIN_PROPERTY, Boolean.TRUE); 

The client application for this example uses a command-line parameter to determine the argument passed to the setUpperCase( ) method, which it calls before invoking any other web service method. The example also retrieves the value to demonstrate that the setting is persistent, and therefore constitutes a client-specific state retained between invocations of the web service's endpoint interface methods:

 boolean upperCase = args.length > 1 && args[1].equalsIgnoreCase("uppercase");
bookQuery.setUpperCase(upperCase);
System.out.println("Upper case? " + bookQuery.isUpperCase(  )); 

You can run the example with the appropriate command line by using the command:

 ant run-client-uppercase 

As a result, you should see the usual list of books, with all of the titles in uppercase.

6.7.6 JAX-RPC Authentication

JAX-RPC does not have any authentication mechanisms of its own. However, you can make use of the generic facilities provided by the hosting web container to provide the same level of security to your web services as are available to other web applications. Both HTTP connections with basic authentication and HTTPS connections (with or without authentication) can be used to carry JAX-RPC traffic, provided that you properly configure the web container and the servlet, and then use the appropriate URL when accessing the service. The topic of web containers and authentication has already been fully discussed in Section 3.8, and, as far as the service implementation is concerned , the setup details for a JAX-RPC service are the same as they are for SAAJ.

In the case of the ContextBookQuery web service, the web.xml file shown in Example 6-34 contains security-constraint and login-config tags that enforce the use of basic authentication and allow access only to users in the contextbookservice role. The roles referred to in the web.xml file, and the users that belong to that role, must be configured with the web container. For the J2EE 1.4 reference implementation, this is done using the realmtool command-line utility, whereas for Tomcat, you need to edit the tomcat-users .xml file. For both of these cases, the details can be found in Chapter 1. If you are using a different application server, you should consult the application server's documentation to find out how to add to its authentication database.

With this protection in place, any attempt to access the URLs provided by this service are rejected unless the client provides the correct authentication information. A JAX-RPC client using either a statically generated stub or a dynamic proxy returned by the Service getPort( ) method can supply the username and password required for HTTP basic authentication by setting two Stub properties:

 String userName = System.getProperty("ContextBooks.user");
String password = System.getProperty("ContextBooks.password");
if (userName != null && password != null) {  stub._setProperty(Stub.USERNAME_PROPERTY, userName);   stub._setProperty(Stub.PASSWORD_PROPERTY, password);  } 

A client using the dynamic invocation interface associates the username and password with the Call object instead:

 Call call = service.createCall(.....);
call.setProperty(Call.USERNAME_PROPERTY, userName);
call.setProperty(Call.PASSWORD_PROPERTY, password); 

Using container authentication in this way allows a web service to delegate the verification of the identify of the client to the container rather than having to be coded to perform this task itself. In many cases, this check provides all the security that the web service requires. If necessary, however, the service can add its own explicit security checks to those performed by the container. Each time a properly authenticated request is made to a servlet, the container creates a java.security.Principal object containing the name of the authenticated user. The implementation methods of a servlet-hosted web service can get access to this object by calling the getUserPrincipal( ) method of its ServletEnpointContext . A web service implemented as a session bean can get the same information from the getCallerPrincipal( ) method of its SessionContext , which is supplied to it during its initialization. The ContextBookQuery web service uses this information to allow only a single user access to the information that it provides. The name of the authorized user is obtained from the initialization parameters of its hosting servlet (see the context-param element in Example 6-34), and the access check is made by the code shown in Example 6-36.

Example 6-36. Using the getUserPrincipal( ) method to get authentication information
 private boolean checkAccess(  ) {
    boolean allowed = true;
    if (userName != null) {
        // Authentication is configured.
        Principal principal = endpointContext.getUserPrincipal(  );
        allowed = principal != null && userName.equals(principal.getName(  ));
    }
    return allowed;            
} 

This check is performed at the start of each method that implements the service endpoint interface. If the calling user is not the one named in the initialization parameters, then the web service implementation method returns a zero count or a null string, depending on the required return type.

The client application for this example obtains the username and password that it should use from properties set on its command line, which in turn come from the jwsnutExamples.properties file in your home directory, as does the name of the authorized user in the servlet initialization parameters. When you run this example, the client application supplies the same username as the one in the web.xml file. Therefore, it passes the web service's authorization check and you get back the complete list of books. If you change the values of the USERNAME and PASSWORD properties in the jwsnutExamples.properties file to those of the other user in the JWSGroup role ”i.e., AnotherUser and Pwd ”then the web container allows the client application to invoke the methods of the web service, since the username and password are valid and correspond to a user in the role JWSGroup . However, because the username is not the same as the one in the web.xml file, the service's checkAccess( ) method returns false. As a result, no book information is returned. If you choose a user that is not in the JWSGroup role, or if you supply an incorrect password, the web container rejects the request before it reaches the web service itself.