See wam Menu

Programming with WAM Services

This section describes how to create your own WAM service using an example service that notifies an administrator via e-mail whenever a user with the manager role logs in.

Because this service must be configured with a valid SMTP mail server, it is disabled by default. See Enabling the WAM Text Notifier Service Example to configure the example for your environment.

The service will be hosted under the WAM system security domain and will be accessed and used from a session event handler created using the WAM Session Event Handler API.

Step 1: Writing the WAM service interface

Example 1 summarizes the Java code for the TextNotifierService interface example.

Here is the full source code for TextNotifierService.java.

Some important points to note about this code include:

  • This interface extends com.cafesoft.core.service.Service. WAM services are managed by a ServiceManager, which requires the service types you register to directly or indirectly extend this interface.

    Failure to follow this rule will result in a java.lang.ClassCastException.

  • The business method: sendText(...) is designed to make it as general enough to support different implementations.

    In particular, the from parameter will have different meanings for an email TextNotifierService versus a text message TextNotifierService, yet either implementation can be registered and looked up by this interface.

package examples.service;

import com.cafesoft.core.service.Service;

public interface TextNotifierService extends Service 
{ 
   /** 
    * Send a textual message. 
    *
    * @param from the message sender. 
    * @param subject the subject of the message. 
    * @param body the body of the textual message.          
    */
   public void sendText(String from, String subject, String body);
   // End of interface: TextNotifierService

Example 1 - TextNotifierService interface code

Step 2: Writing the WAM service implementation class

Writing a WAM service implementation class can be roughly broken down into the following steps:

If your service implements a LifecycleService because it can benefit from being started and stopped, then the following step also applies: Writing the WAM service start and stop methods

Declaring the WAM service class

Example 2 shows how you can declare your WAM service implementation class to take advantage of methods in the AbstractService class that ships with WAM.

This abstract class contains ready-to-use code for the most general WAM service methods. You can override the methods in your implementation class that require customization.

Here is the full source code for class SmtpTextNotifierService.java.

Package examples.service;

import com.cafesoft.core.service.AbstractService;

import examples.service.TextNofifierService;

public class SmtpTextNotifierService 
   extends AbstractService 
   implements TextNotifierService 
{
   ...
}

Example 2 - Declaring the SmtpTextNotifierService class

Writing the WAM service initialize method

Example 3 shows how you can initialize your WAM service implementation. In this case, the code overrides the initialize() method from AbstractService to check for required configuration parameters.

Some important points within this example include:

  • The initialize() method of superclass com.cafesoft.core.service.AbstractService is invoked to properly setup class variables available to all subclasses.

  • Configuration parameters are obtained from the ServiceConfig object passed as a parameter to the initialize() method.

  • Invoking super.initialize(serviceConfig) will cause AbstractService to make class variables serviceConfig, context, logger, and debug available to all subclasses.

  • If your service cannot be properly initialized, it should throw a ServiceException.

/**
 * Override the initialize method to check for required config parameters.
 *
 * @param serviceConfig the object through which configuration parameters and
 *  other resources are available.
 * @exception ServiceException if one or more configuration parameters
 *  are missing.
 */
Public void initialize(ServiceConfig serviceConfig) throws ServiceException
{
  // Initialize state in the abstract base class
  super.initialize(serviceConfig);


  // Complain if required parameters are missing
  this.smtpHost = serviceConfig.getInitParameter("smtp.host");
  if ((smtpHost == null) || (smtpHost.trim().length() == 0))
    throw new ServiceException("Required parameter: 'smtp.host' " +
      "is missing or empty");

  this.smtpTo = serviceConfig.getInitParameter("smtp.to");
  if ((smtpTo == null) || (smtpTo.trim()Length() == 0))
    throw new ServiceException("Required parameter: 'smtp.to' " +
      "is missing or empty");
}

Example 3 - Initializing the SmtpTextNotifierService

Writing the WAM service business method(s)

Example 4 shows the code that implements the sendText() business method for SmtpTextNotifierService.

Important issues to note include:

  • The use of the logger object to report DEBUG and ERROR messages.

    The first parameter, (this), is used to report the class and line number from which the debug message is issued.

    If an Exception needs to be reported, Logger contains overloaded methods debug(), info(), warning(), error(), and fatal(), with a java.lang.Throwable as the third parameter.

  • SmtpClient is an internal WAM utility used here for demonstration purposes only.

  • Only one instance of this service is available with the security domain’s service manager, so it must be written to be thread-safe.

    If state must be maintained between method invocations on your service, do so in the calling code or use java.lang.ThreadLocal inside your service implementation.

  • Design WAM services to be as fast and efficient as possible.

    If the service implements slow actions (like sending email), and the calling code does not need to wait for a response, consider running the expensive code in a new Thread as shown in Example 4.

/**
 * Send a textual message.
 *
 * @param from the message sender.
 * @param subject the subject of the message.
 * @param body the body of the textual message.
 */
public void sendText(String from, String subject, String body)
{
  // Sending an SMTP message is slow, so create a new Thread and
  // send the message asynchronously.
  SmtpClientRunnable r = new SmtpClientRunnable(from, subject, body);
  Thread t = new Thread(r);
  t.start();
}

//
// inner classes
//

/**
 * SmtpClientRunnable implements a Runnable class
 * for asynchronously sending a message to an SMTP server.
 */
private class SmtpClientRunnable implements Runnable
{
  private String from;
  private String subject;
  private String body;

 /**
   * Create a new ManagerLoginNofifierRunnable.
   *
   * @param from the message sender.
   * @param subject the subject of the message.
   * @param body the body of the textual message.
   */
  public SmtpClientRunnable(String from, String subject, String body)
  {
    this.from = from;
    this.subject = subject;
    this.body = body;
  }

 //
  // implementing Runnable
  //

 /**
   * Send the text message via SMTP.
   */
  Public void run()
  {
    try
    {
      if (debug)
        logger.debug(this, "Connecting to SMTP server=" + 
          smtpHost +
          ", from=" + from +
          ", to=" + smtpTo +
          ", subject=" + subject);

      // Setup the mail transport host
      SmtpClient c = new SmtpClient(smtpHost, false);
      c.from(from);
      c.to(smtpTo);

      // Send the message
      PrintStream ps = c.startMessage();
      ps.print("To: " + smtpTo + "\n");
      ps.print("From: " + from + "\n");
      ps.print("Subject: " + subject + "\n");
      ps.print("X-Client: com.cafesoft.core.smtp.SmtpClient" + "\n");
      ps.print("\n");
      ps.print(body);
      ps.print("\n");

      // Close connection with the SMTP server
      c.closeServer();

      if (debug)
        logger.debug(this, "Successfully sent text message");
    }
    catch (UnknownHostException e)
    {
      logger.error(this, "Unknown SMTP server host=" + smtpHost, e);
    }
    catch (UnknownUserException e)
    {
      logger.error(this, "Unknown message recipient=" + smtpTo, e);
    }
    catch (IOException e)
    {
      logger.error(this, "Error sending message", e);
    }
  }
} // End of class: SmtpClientRunnable

Example 4 - Implementing the SmtpTextNotifierService business method

Writing the WAM service destroy method

The WAM service destroy() method is responsible for cleaning up any resources referenced by a WAM service before it is destroyed.

It will be invoked by the WAM service manager when the enclosing security domain’s service manager is destroyed. This occurs when the WAM Policy Server exits and when the enclosing security domain is destroyed.

Example 5 shows the destroy method for SmtpTextNotifierService.

Here are some important points to note within this example:

  • In general, any Objects referenced by your WAM service implementation should be set to null.

  • Invoking super.destroy() will cause AbstractService to dereference class variables that it has defined, namely serviceConfig, context, logger, and debug, which are available to all subclasses.

    Be sure to invoke this after you are done logging any messages from your destroy method.

/**
 * Destroy the service.
 */
Public void destroy()
{
  // null local Object references.
  this.smtpHost = null;
  this.smtpTo = null;

  // Invoke the super class destroy method.
  super.destroy();
}

Example 5 - Destroying the SmtpTextNotifierService

Writing the WAM service start and stop methods

In some cases, it may be useful to implement a WAM service as a LifecycleService. This interface includes start() and stop() methods that are invoked when the enclosing security domain’s service manager is started and stopped.

Implementing these method gives your service the opportunity to:

  • Start and stop without being reinitialized.

  • Notify components that use or manage LifecycleServices of start/stop actions.

The SmtpTextNotifierService example is not a LifecycleService, so it does not implement start and stop methods, but you can see an example in StandardRdbmsService.java.

Step 3 - Compiling and deploying the classes to the WAM Policy Server classpath

The following commands show how you can compile your WAM services classes and deploy them to a directory where the WAM policy server will automatically find and load them.

The examples assume that environment variable CAMS_HOME is defined and points to the camsServer directory within the WAM distribution.

  • Unix: javac -classpath .:$CAMS_HOME/lib/cams.jar:$CAMS_HOME/lib/cscore.jar -d $CAMS_HOME/classes *.java

  • Windows: javac -classpath .;%CAMS_HOME%\lib\cams.jar;%CAMS_HOME%\lib\cscore.jar -d %CAMS_HOME%\classes *.java

You’ll need to shutdown and restart the WAM Policy Server for changes to take effect. For production use, you may want to jar your components, drop them in directory $CAMS_HOME/lib, and modify script runcams.sh or runcams.bat to add the jar file to the WAM Policy Server’s classpath.

Step 4 - Registering the service with a WAM security domain

Example 6 shows how you might register and configure this service for use by other custom components (or other services) within a security domain.

This service can be looked up by its identifier email-text-notifier-service and/or by its type com.myco.message.TextNotifier.

<service id="email-text-notifier-service" enabled="true">
  <service-type>com.myco.message.TextNotifierService</service-type>
  <service-class>com.myco.message.SmtpTextNotifierService</service-class>
  <param-list>
    <param name="smtp.host" value="mycompany.com">
    <param name="smtp.to" value="security-admin@mycompany.com">
  </param-list>
</service>

Example 6 - Registering the email-text-notifier-service within a security domain

Step 5 - Finding and using a WAM service from other components

The WAM services deployed within the service manager of a security domain can be found and invoked by WAM LoginModules, Access Control Rules, Managed Session Event Handlers, and other WAM Services within that security domain. WAM services are looked up via a ServiceFinder object, which is made available to WAM components.

Getting a WAM ServiceFinder in LoginModules

WAM supports JAAS-compatible LoginModules. For the sake of performance and scalability, some of the LoginModules shipped with WAM, such as XmlLoginModule, also implement a WAM interface that enables them to make use of WAM services.

The XmlLoginModule makes use of an UserRepository WAM service, which allows WAM to authenticate users using cams-users.xml as its user repository.

Example 7 shows the code that causes LoginModules to be given a ServiceFinder before any other LoginModule methods are invoked.

The important points in this code include:

  • The LoginModule declares and implements the ServiceClient interface.

  • The LoginModules saves a reference to the ServiceFinder for later on from other methods.


import javax.security.auth.spi.LoginModule;


import com.cafesoft.core.service.Service;
import com.cafesoft.core.service.ServiceClient;
import com.cafesoft.core.service.ServiceException;
import com.cafesoft.core.service.ServiceFinder;


public class XmlLoginModule implements LoginModule, ServiceClient
{
  ...

  /** Used to lookup WAM services. */
  private ServiceFinder serviceFinder;

  ...

  //
  // implementing ServiceClient
  //
  /**
   * Set the ServiceFinder.
   *
   * @param serviceFinder the class used to find WAM Services
   */
  public void setServiceFinder(ServiceFinder serviceFinder)
  {
    this.serviceFinder = serviceFinder;
  }

  ...
}

Example 7 - Implementing the ServiceClient interface in a WAM LoginModule

Getting a WAM ServiceFinder from Access Control Rules and Managed Session Event Handlers

WAM access control rules and managed session event handers are initialized with a com.cafesoft.cams.Config Object that references a com.cafesoft.cams.Context Object.

A ServiceFinder is available from the Context Object as shown in Example 8. Of course, you don’t really need to save a reference to the Context and ServiceFinder Objects because they are always available from the Config Object.

import com.cafesoft.core.Config;
import com.cafesoft.core.Context;
import com.cafesoft.core.ConfigException;


import com.cafesoft.core.service.Service;
import com.cafesoft.core.service.ServiceClient;
import com.cafesoft.core.service.ServiceException;
import com.cafesoft.core.service.ServiceFinder;


public void initialize(Config config) throws ConfigException
{
   this.config = config;
   this.context = config.getContext();
   this.serviceFinder = context.getServiceFinder();
  ...
}

Example 8 - Getting a ServiceFinder in AccessControlRule and ManagedSessioEventHandlers

Getting a WAM ServiceFinder from WAM services

WAM services are initialized with a com.cafesoft.core.service.ServiceConfig Object that references a com.cafesoft.core.service.ServiceContext Object.

A ServiceFinder is available from the ServiceContext Object as shown in Example 9.

import com.cafesoft.core.service.ServiceConfig;
import com.cafesoft.core.service.ServiceContext;


import com.cafesoft.core.service.Service;
import com.cafesoft.core.service.ServiceClient;
import com.cafesoft.core.service.ServiceException;
import com.cafesoft.core.service.ServiceFinder;


public void initialize(ServiceConfig serviceConfig) throws ServiceException
{
   this.serviceConfig = serviceConfig;
   this.serviceContext = serviceConfig.getServiceContext();
   this.serviceFinder = serviceContext.getServiceFinder();
  ...
}

Example 9 - Getting a ServiceFinder in a WAM Service

Finding and using a WAM Service

Once you’ve got a ServiceFinder, finding and using a WAM service is easy. The ServiceFinder provides multiple ways to find a Service instance:

  • All Service instances matching a registered type.

  • A Service instance by identifier and type.

  • A Service instance by identifier.

Finding all Services instances by type

Example 10 shows how you can find and invoke all Service instances by a specific type.

Important points to note include:

  • The code assumes that variable serviceFinder was set as shown in Example 7, 8, or 9 depending on the type of WAM component needing to use the Service.

  • The WAM service type is the value TextNotifierService.class, which is the Java Class Object by which the service was registered.

    See the service-type XML element in section: Registering the service with a WAM security domain.

  • When attempting to WAM services by type alone, zero or more instances of that type may have been registered.

    For example, SmtpNotifierService and a PageNotifierService might be registered with type TextNotifierService. In this case, both instances would be returned in the Service array.

  • To invoke a business method on a Service, you’ll need to cast it to its appropriate type.

  • If a Service by the requested type is not registered, a ServiceException is thrown.

import com.cafesoft.core.service.Service;

import com.cafesoft.core.service.ServiceException;
import com.cafesoft.core.service.ServiceFinder;


import examples.session.TextNotifierService;


  ...


  try
  {
    // Lookup the TextNotifierService instance(s) by type
    Service[] serviceArray = serviceFinder.find(TextNotifierService.class);

    // Send the message to every available TextNotifierService
    String body = createMessageBody(event);
    for (int i = 0; i < serviceArray.length; i++)
    {
      TextNotifierService tns = (TextNotifierService)serviceArray[i];
      tns.sendText(fromAddress, msgSubject, body);
    }
  }
  catch (ServiceException e)
  {
    logger.error(this, "Unable to find TextNotifierService");
  }

...

Example 10 - Finding and using all WAM Service by type

Finding a specific Service instance by identifier and type

Example 11 shows how you can find and invoke a specific Service instances by a specific type and identifier.

Important points to note include:

  • The code assumes that variable serviceFinder was set as shown in Example 7, 8, or 9 depending on the type of WAM component needing to use the Service.

  • The WAM service type is the value TextNotifierService.class, which is the Java Class Object by which the service was registered.

    See the service-type XML element in section Registering the service with a WAM security domain.

  • The service identifier is case-sensitive and must be specified exactly as registered with the security domain.

  • A WAM service registration is unique by type and identifier, so at most one instance will be returned.

  • To invoke a business method on a Service, you’ll need to cast it to its appropriate type.

  • If a Service by the requested type and identifier is not registered, a ServiceException is thrown.

import com.cafesoft.core.service.Service;

import com.cafesoft.core.service.ServiceException;
import com.cafesoft.core.service.ServiceFinder;


import examples.session.TextNotifierService;


  ...


  try
  {
    // Lookup the TextNotifierService instance(s) by type and ID
    TextNotifierService tns = (TextNotifierService)serviceFinder.find(
      "email-text-notifier-service", TextNotifierService.class);

    // Send the message to the TextNotifierService
    String body = createMessageBody(event);
    tns.sendText(fromAddress, msgSubject, body);
  }
  catch (ServiceException e)
  {
    logger.error(this, "Unable to find TextNotifierService " +
      "with id=email-text-notifier-service");
  }

...

Example 11 - Finding a WAM Service instance by identifier and type

Finding a Service by identifier

Example 12 shows how you can find and invoke a specific Service instances by identifier only.

Important points to note include:

  • The code assumes that variable serviceFinder was set as shown in Example 7, 8, or 9 depending on the type of WAM component needing to use the Service.

  • The service identifier is case-sensitive and must be specified exactly as registered with the security domain.

  • A WAM service registration is unique by identifier, so at most one instance will be returned.

  • To invoke a business method on a Service, you’ll need to cast it to its appropriate type.

    However, be careful, if the returned Service does not implement the cast type, a java.lang.ClassCastException will be thrown.

  • If a Service by the requested identifier is not registered, a ServiceException is thrown.

import com.cafesoft.core.service.Service;

import com.cafesoft.core.service.ServiceException;
import com.cafesoft.core.service.ServiceFinder;


import examples.session.TextNotifierService;


  ...


  try
  {
    // Lookup the TextNotifierService instance(s) by type and ID
    Service service = serviceFinder.find("email-text-notifier-service");

    // Make sure it implements TextNotifierService
    if (!(service instanceof TextNotifierService))
    {
      logger.error(this, "The Service with id=email-text-notifier-service " +
        "does not implement type: " + TextNotifierService.class.getName());
      return;
    }

    // Send the message to the TextNotifierService
    String body = createMessageBody(event);
    ((TextNotifierService)service).sendText(fromAddress, msgSubject, body);
  }
  catch (ServiceException e)
  {
    logger.error(this, "Unable to find TextNotifierService " +
      "with id=email-text-notifier-service");
  }

...

Example 12 - Finding a WAM Service instance by identifier

Step 6 - Starting or restarting the WAM policy server to load the service

For the service to be loaded, you must restart the WAM policy server if it is currently running.

You should use the shutdown.bat/shutdown.sh command in the bin directory of WAM to shutdown the server. Then, use runcams.bat/runcams.sh to start it.


Have a Question?

Have a how-to question? Seeing a weird error? Contact us.

Found a bug? Submit a support ticket.

Have a product idea or request? Share it with us in our Ideas Portal.