Writing Server-side Command Listeners

In This Section:

Prerequisites

Command Listeners

Command Handling Methods

Packaging the Code

Loading the Code

Utility Classes

This chapter explains how to write a command listener for the Administration Services mid-tier web server. Installable command listeners are the mechanism for extending the functionality of the Administration Services Web server.

Prerequisites

You should have the following skills and tools:

  • You have some Java experience

  • You have access to the Administration Services Java API Reference

  • Since different developers use different build tools and environments, we do not discuss how to do anything for specific development environments. Rather, we describe the desired results, leaving it to the developer to know how to achieve these results with their specific development tools.

    Note:

    For the purposes of this documentation, the terms “Administration Services web server”, “Administration Services servlet”, “Administration Services mid-tier”, “Administration Services framework”, and, simply, “the framework” can generally be taken to refer to the same object.

    The framework is the Administration Services servlet and associated classes that receive commands, handle housekeeping duties, return results, and route commands to the registered listener.

Command Listeners

A command listener is an instance of any class that implements the CommandListener interface; however, for practical purposes, all plug-in command listeners should extend one of these classes:

  • EssbaseCommandListener

  • AppManCommandListener

  • AbstractCommandListener

The framework uses command listeners as the mechanism to properly route commands to be handled.

When the Administration Services servlet starts up, it builds a table of command listeners, the commands that each command listener can handle, and the method in the command listener for that command. As client applications send commands (http requests), theAdministration Services servlet uses the command's operation parameter to determine the command listener and method to route the request to.

For example, a typical command might be to log in to the Administration Services servlet. When expressed as an http request, this command will look something like this:

http://LocalHost/EAS?op=login&name=user1&password=hello

The Administration Services servlet parses the following parameters:

  • op=login

  • name=user1

  • password=hello

The framework uses the “op” parameter to route the command to the correct command listener. If the command listener has been registered correctly, the framework will also collect the “name=” and “password=” parameters and pass them as arguments to the method in the command listener.

Class Hierarchy

The class hierarchy for the command listeners is:

com.essbase.eas.framework.server.application.AbstractCommandListener
com.essbase.eas.server.AppManCommandListener
com.essbase.eas.essbase.server.EssbaseCommandListener

All three of these classes are declared as abstract. You must extend from one of these three classes in order to have the framework find your command listener.

The AbstractCommandListener class provides the basic functionality that is needed for the framework. Most of the methods in this class are either final or protected; for most practical purposes, implementers of derived classes should not override the protected methods of this class. For a description of those methods that can be useful to implement in a derived class, see the section Which Methods to Override.

The AppManCommandListener class adds some small functionality to the AbstractCommandListener, mostly dealing with EAS servlet session validation and exception handling during command routing.

The EssbaseCommandListener class adds some Essbase-specific functionality, primarily Oracle Essbase session validation.

Which Class To Extend

Do not extend the AbstractCommandListener class, even though it is declared public. The EssbaseCommandListener.handleEventPrep() method checks some standard parameters for an Essbase Server name, application name, and database name and ensures a connection to that database if those parameters exist. If the implementer of the new command listener wishes to take advantage of the session handling performed by the EssbaseCommandListener, then they should extend this class; however, if this isn’t necessary, the new command listener can extend the AppManCommandListener class.

Which Methods to Override

AbstractCommandListener.getCommands() must be overridden. We explain more about this method in the section, Registering Commands.

The handleEventPrep(), handleEventPost(), and handleEventException() methods may be overridden. These three methods, along with AbstractCommandListener.handleEvent(), form the core processing for any command received by the framework.

Once the framework determines which command listener to route a command to, it calls that command listener’s handleEvent() method. Since the AbstractCommandListener declares this method as final, the framework always calls the method in AbstractCommandListener. This method then performs the following sequence of steps:

  1. Calls handleEventPrep(); if this method returns true, then continues with step 2.

  2. Gets the command listener's method that handles this specific command. If this method cannot be located, logs an error with the logging utility.

  3. Converts the arguments from the http command into an array of Java objects.

  4. Using Java introspection, invokes the method.

  5. If no exceptions were thrown, invokes handleEventPost().

  6. If exceptions were thrown in steps 4 or 5, calls handleEventException().

Any change to the processing of events before they arrive at a specific method in the command listener must be done by overriding the handleEventPrep() method. For instance, this is where the EssbaseCommandListener class checks Essbase sessions and the AppManCommandListener checks for a valid servlet session.

In most cases, the handleEventPost() method is empty and the handleEventException() method is empty.

Registering Commands

After a command listener is instantiated by the framework, the framework calls the getCommands() method. This method returns an array of CommandDescriptor objects. The CommandDescriptor objects describe each command that the CommandListener is designed to handle. The CommandDescriptor object consists of three main parts:

  • A string for the command

  • The method in the command listener to call

  • The list of arguments expected for this command.

The next few sections describe the classes used by the framework when registering commands.

Note:

All of these classes are in the package com.essbase.eas.framework.defs.command.

CommandString Class

A command listener handles commands like “GetDatabaseList”, “GetUsers”, “DeleteUsers”, and so on. The CommandString class was introduced to let each command listener programmer think of their commands in the simplest way. The CommandString class is declared as:

public abstract class CommandString

The only constructors are declared as:

private CommandString() { ... }
protected CommandString(String original) { ... }

These two declarations combined mean that instances of this class can never be instantiated and derived classes must call the CommandString(String original) constructor with a valid String object as the parameter.

The most important action that instances of this class do is take the original String object and prepend the class name, including the package name, to the front of the String. This new value is then returned when the object’s toString() method is called.

CommandArgument Class

The CommandArgument class describes individual arguments to commands. It contains the following fields:

  • String name (available through the getName() method)

    This is the name of the http parameter corresponding to this argument.

  • boolean required (available through the isRequired() method)

    Indicates whether this argument is required. The intent is that the framework can check this field when routing a command and return a pre-defined error status to the client if a required field is missing.

  • Class ClassType (available through the getClassType() method)

    This is used so the framework can convert the incoming text value to an appropriate object type.

  • Object defaultValue (available through the getDefaultValue() method)

    The framework will substitute this object for the argument if the argument is missing from the command.

  • Boolean hidden (available through the isHidden() method)

    The framework can log the retrieval and routing of commands and their parameters. Setting this field to true means the framework will not echo the value of this argument in the log file. This would be useful for passwords, and so on.

These fields are all declared as private and, since there are no setXXX() methods, cannot be changed after a CommandArgument object is constructed.

CommandDescriptor Class

The CommandDescriptor class combines the CommandArgument and CommandString classes into a cohesive value so that the framework can construct its internal tables and route the commands as they are received.

The examples in the following sections show how all of this fits together.

Examples

This section includes the following sample code:

Example.java

// this is a simple class used as a parameter to show how the
// framework can separate out command arguments that are object
// types embedded in XML. For more information on how the
// framework uses XML to transport "generic" objects between the
// mid-tier and the client, please see the Java Docs references
// for the XMLTransferObject class.
public Example extends Object {
  private String name = "";
  private String[] text = new String[0];
  // no-argument constructor. Must be public for XML Transfer
  // to work.
  public Example() {
  }
  
  public String getName() {
    return name;
  }
  
  public void setName(String value) {
    name = value;
  }
  
  public String[] getSampleText() {
    String[] result = new String[text.length];
    for (int i = 0; i < result.length; ++i)
      result[i] = text[i];
    return result;
  }
  
  public void setSampleText(String[] values) {
    if (values != null) {
      text = new String[values.length];
      for (int i = 0; i < values.length; ++i)
        text[i] = values[i];
    }
    else {
        text = new String[0];
    }
  }
}

ExampleCommandString.java

public ExampleCommandString extends CommandString {
    // declare some static String objects in a way that we know these							
    // objects do not need to be translated to different locales.
    public static final String GET_EXAMPLES_TEXT = "GetExamples";
    public static final String ADD_EXAMPLE_TEXT = "AddExample";
    public static final String DELETE_EXAMPLE_TEXT = "DeleteExample";							
    
    // now we declare the actual commands
    public static final ExampleCommandString GET_EXAMPLES =
        new ExampleCommandString(GET_EXAMPLES_TEXT);	
    public static final ExampleCommandString ADD_EXAMPLE =
        new ExampleCommandString(ADD_EXAMPLE_TEXT);	
    public static final ExampleCommandString DELETE_EXAMPLE =
        new ExampleCommandString(DELETE_EXAMPLE_TEXT);	
        
    // for organizational purposes, we also declare the parameters for each
    // of these commands in this file.
    public static final String PARAM_LOCATION = "location";
    public static final String PARAM_EXAMPLE = "example";
    public static final String PARAM_NAME = "examplename";
    
    // declare a CommandArgument object for each of these parameters
    private static final CommandArgument ARGUMENT_LOCATION =
        new CommandArgument(PARAM_LOCATION,
        true,
        String.class,
        null);
    private static final CommandArgument ARGUMENT_EXAMPLE =
        new CommandArgument(PARAM_EXAMPLE,
        true,
        Example.class,
        null);
    private static final CommandArgument ARGUMENT_NAME =
        new CommandArgument(PARAM_NAME,
        true,
        String.class,
        null);
    
    // declare an array of arguments for each command.
    public static final CommandArgument[] GET_EXAMPLES_ARGS =
        new CommandArgument[] { ARGUMENT_LOCATION };	
    public static final CommandArgument[] ADD_EXAMPLE_ARGS =
        new CommandArgument[] { ARGUMENT_LOCATION,	
            ARGUMENT_EXAMPLE };
    public static final CommandArgument[] DELETE_EXAMPLE_ARGS =
        New CommandArgument[] { ARGUMENT_LOCATION,	
            ARGUMENT_NAME };
}

This class declares command strings and describes the arguments for three commands that will be supported by the ExampleCommandListener class. If the toString() method of each ExampleCommandString object declared in this source code file were called, the results would be:

ExampleCommandString.GetExamples
ExampleCommandString.AddExample
ExampleCommandString.DeleteExample 

Every CommandDescriptor object contains a reference to an object derived from CommandString; it is through this mechanism that the framework guarantees every command name is unique.

ExampleDescriptor.java

public class ExampleDescriptor extends CommandDescriptor {
    private static final String GET_EXAMPLES_METHOD = "getExamples";
    private static final String ADD_EXAMPLE_METHOD = "addExample";
    private static final String DELETE_EXAMPLE_METHOD = "deleteExample";
    
    public static final CommandDescriptor GET_EXAMPLES =
        new CommandDescriptor(ExampleCommands.GET_EXAMPLES,			
            GET_EXAMPLES_METHOD,
            ExampleCommands.GET_EXAMPLES_ARGS);
    public static final CommandDescriptor ADD_EXAMPLE =
        new CommandDescriptor(ExampleCommands.ADD_EXAMPLE,
            ADD_EXAMPLE_METHOD,
            ExampleCommands.ADD_EXAMPLE_ARGS);
    public static final CommandDescriptor DELETE_EXAMPLE =
        new CommandDescriptor(ExampleCommands.DELETE_EXAMPLE,
            DELETE_EXAMPLE_METHOD,
            ExampleCommands.DELETE_EXAMPLE_ARGS);
}

ExampleCommandListener.java

public class ExampleCommandListener extends AppManCommandListener {
    // the method called when the GetExamples command is received.
    public boolean getExamples(CommandEvent theEvent,
            ServiceContext theContext,
            String theLocation) {
        // the details will be filled in later
        return true;
    }
    
    // the method called when the AddExample command is received.
    Public Boolean addExample(CommandEvent theEvent,
            ServiceContext theContext,
            String theLocation,
            Example theExample) {
        // the details will be filled in later
        return true;
    }
    
    // the method called when the DeleteExample command is
    // received.
    public boolean deleteExample(CommandEvent theEvent,
            ServiceContext theContext,
            String theLocation,
            String theName) {
        // the details will be filled in later.
        return true;
    }
    
    // the framework calls this method to get the descriptors for
    // the commands supported by this command listener.
    public CommandDescriptor[] getCommands() {
        return new CommandDescriptor[] {
            ExampleDescriptor.GET_EXAMPLES,
            ExampleDescriptor.ADD_EXAMPLE,
            ExampleDescriptor.DELETE_EXAMPLE };
    }
}

The preceding example shows the skeleton of a command listener:

  1. Extend the correct class

  2. Add the command handling methods

  3. Override the getCommands() method to return the descriptors for those commands.

The difficulty is in the details of the command handling methods, which is covered in the next section.

Command Handling Methods

This section includes the following topics:

Method Signatures

If you were looking carefully at the example code in the preceding section, you might be saying something along the lines of, “Wait a minute, in GET_EXAMPLES_ARGS, I defined one argument, the location argument. What are these other two arguments, theEvent and theContext? Where did they come from and what do I do with them?” The answer partly lies in the older version of the Administration Services framework. The first version of the framework did not do all the type checking and parameter parsing that the new level does, so all command handling methods had the following signature:

  public boolean handlerMethod(CommandEvent theEvent) { }

It was up to each method to then extract and handle the arguments along the lines of:

  String theLocation = theEvent.getRawParameter("Location");		
  if (theLocation == null) {
    // oops - this shouldn't happen!
    return false;
  }

Or, if the parameter was supposed to be a numeric value:

  int theNumber = 0;
  String theValue = theEvent.getRawParameter("Value");
  if (theValue == null) {
    // oops - this shouldn't happen!
    return false;
  }
  try {
    theNumber = Integer.parseInt(theValue)
  }
  catch (Exception ex) {
    return false;
  }

In most cases, theEvent object was used mostly to get the parameters for the command. When the framework was upgraded, theEvent object was retained as the first argument to the command handler methods, even though it is rarely used.

The second argument, theContext, is actually a field in theEvent object; if you want to return results to the client, you must do so through the ServiceContext reference. Since every command handling method at some time would call theEvent.getServiceContext(), we decided to add it as a second parameter to every command handling method.

As a result of these decisions, every command handling method has the following signature:

  public boolean handlerMethod(CommandEvent theEvent,
          ServiceContext theContext,
          Class0 value0,
          ...,
          ClassN, ValueN);

Where the ClassX parameters are described by the CommandDescriptor for the method.

In addition, even though the method is declared boolean, the framework never looks at the return value from a command handler method. Return values are handled within each method by a mechanism explained later in this document.

Grabbing Command Arguments

In most cases, the command arguments will have been extracted and parsed by the framework; however, special circumstances can arise whereby extra arguments are sent with each command that, for whatever reason, the programmer doesn't want to include in the CommandDescriptor object.

An example is the EssbaseCommandListener; the EssbaseCommandListener.handleEventPrep() method calls a validateSession() method that looks for the standard parameters “servername”, “appname”, “dbname”, then attempts to validate an EssbaseSession against those parameters. If this fails, then the handleEventPrep() method returns a standard error status to the client. In most cases, any EssbaseCommandListener will need these arguments when handling commands. However, there are cases (such as in the outline editor) when those arguments aren't used. If, during implementation of a command listener method, a similar situation arises, the parameters can be retrieved by the following call:

  String aValue = theEvent.getRawParameter("SomeParameterName");

This should be a rare necessity and should raise caution alarms if an implementer finds themselves needing to do this.

Sending Results Back to the Client

There are two types of results to return to a client:

  1. Status of the command

  2. Data that the client needs to display

The CommandStatus class is used to return the success/failure of the command to the client. The CommandStatus class only understands two types of status: SUCCESS and FAILURE. The original intent of this class was to indicate whether a command was routed successfully by the framework. However, this wasn’t made explicit and, as a result, many existing command handling methods use this SUCCESS/FAILURE to indicate the status of their specific processing.

It would be a good practice to always extend this class to enable returning more specific error codes than just SUCCESS/FAILURE.

So, let’s return to our example and fill in one of the command handling methods to return data and a SUCCESSFUL status to the client.

public boolean getExamples(CommandEvent theEvent,
          ServiceContext theContext,
          String theLocation) {
  //object used to transmit results back to the client
  XMLTransferObject xto=new XMLTransferObject();
  Example [] theResults=someMethod(theLocation);
  if (theResults == null) {
    //this is simplistic, but it shows what we need
    xto.setCommandStatus(CommandStatus.SIMPLE_FAILURE);
  }
  else {
    if (theResults.length != 0)
      xto.addAll(theResults);
    xto.setCommandStatus(CommandStatus.SIMPLE_SUCCESS);
  }
  this.storeService.set(theContext,
          DefaultScopeType.REQUEST_SCOPE,
          AppManServlet.RESULT,
          xto.exportXml());
  return true;
}

The XMLTransferObject is used to transmit the data and the command status back to the client; we use the defined CommandStatus.SIMPLE_FAILURE or CommandStatus.SIMPLE_SUCCESS objects to return the correct status. If results were available, they were then added to the XMLTransferObject using the addAll() method. The results were then placed in the command listener's store service using the REQUEST_SCOPE and using the key AppManServlet.RESULT. After this method returns to the framework, the framework will take any data stored using the combination DefaultScopeType.REQUEST_SCOPE and AppManServlet.RESULT and send that data back to the client as the results of the command.

Storing Temporary Data Using the Framework

In the preceding section, we gave an example of how to place data in the framework’s storage so that the data would be returned to the client as the results of a command. The storeService field in each command manager can store data for additional purposes. There are six defined DefaultScopeTypes:

  1. CONFIG_SCOPE

    This is used by the framework as it is initializing. It should never be used by command handler methods.

  2. BUILDER_SCOPE

    This is used by the framework as it is initializing. It should never be used by command handler methods.

  3. APP_SCOPE

    Using this scope type will cause the data to be stored for the life of the servlet. This should be very, very rarely used by command listeners.

  4. SESSION_SCOPE

    Using this scope type will cause the data to be stored until the current client/server session is no longer valid. At that point, the framework will remove all data stored in this scope. Store information in this scope that needs to be recovered when processing subsequent commands.

  5. USER_SCOPE

    Using this scope makes the data available to any client connected using the same EAS user id. When all sessions associated with this user are no longer valid, the framework will remove data stored in this scope. In the current implementation, this is never used and it probably will never be used very often.

  6. REQUEST_SCOPE

    Using this scope makes the data available until the framework has bundled the results of the command and returned them to the client. The framework then removes all data stored in this scope associated with the request that just finished.

Storing data is done through a command listener’s store service, as in the preceding example. The StoreService interface has several get(), set(), and remove() methods. However, there is only one of each of these methods that a command listener (or other plug-in code) should call; the other methods were put in place for use by some of the framework code itself. The three method signatures are:

public Object get(ServiceContext context, ScopeType type, Object key);
public Object set(ServiceContext context, ScopeType type, Object key, Object value);
public Object remove(ServiceContext context, ScopeType type, Object key);

For more information about these methods, see the Essbase Administration Services Java API Reference.

Packaging the Code

When packaging the code into jar files for a plug-in, follow these guidelines:

  • Separate the code into three distinct pieces:

    • Code that is only used on the client

    • Code that is only used on the server

    • Code that is used in both places

  • Set up the build tools to compile and package these pieces separately to prevent crossover compilation. For example, the framework is packaged into the following jar files:

       framework_client.jar
       framework_common.jar
       framework_server.jar
  • Package the command listener classes in the server jar

  • Package the command descriptor classes in the server jar. This is because they contain references to the method names in the command listeners and this should not be publicly available on the client.

  • Package the CommandString derived classes in the common jar file. While the framework does not currently take advantage of this on the client, it will be upgraded to do the packaging of parameters and commands for client applications.

  • Place any classes extending CommandStatus in the common jar file.

  • Place any specialized classes (such as Example.java) in the common jar file.

The server jar file must contain a manifest file. Each command listener must have an entry in this manifest file that looks like the following:

Name: ExampleCommandListener.class

EAS-Framework-CommandListener: True

If, as is likely, the command listener has a package name that must be prepended to the name in the example above, like this:

Name: com/essbase/eas/examples/server/ExampleCommandListener.class

EAS-Framework-CommandListener: True

Note:

Even though this is a class name, use slashes (“/”) instead of dots (“.”) to separate the package names.

Loading the Code

To enable the framework to recognize command listeners and route commands to the correct place, the jar file containing the command listeners and any other jar files on which this code depends must be bundled inside Administration Server’s eas.war file. The eas.war file is contained in the eas.ear file, and must be unpacked so that the command listeners can be added.

  To bundle the command listeners:

  1. Unzip the eas.ear file.

  2. Unzip the eas.war file.

  3. In the directory structure thus created, add the command listeners to the WEB-INF/lib directory.

  4. Rezip the eas.war file.

  5. Rezip the eas.ear file.

Note:

The eas.ear file is installed by default to EPM_ORACLE_HOME\products\Essbase\eas\server\AppServer\InstallableApps\Common.

After putting the jar files in this location, you must stop and restart Administration Server. To determine if the new command listeners have been installed, set the Administration Services logging level between INFO and ALL.

Utility Classes

There are many utility classes provided by theOracle Essbase Administration Services framework. In particular, there are utility classes in some of the following packages:

com.essbase.eas.framework.defs
com.essbase.eas.framework.server
com.essbase.eas.utils
com.essbase.eas.ui 
com.essbaes.eas.i18n
com.essbase.eas.net

The Administration Services Java API Reference makes it easy to navigate through these classes and learn what is available.