15.7. Anatomy of SimpleSdt: the "ShellCommand class"

So far we've looked at the XML definition file, which declares the abstract structure of the SDT, and the wrapper class, which implements a Java class providing an object-oriented interface to each instance of sensor data associated with the SDT. To complete the basic implementation of an SDT, we must provide a class that facilitates the transmission of sensor data of this type from the client-side sensor to the server-side Hackystat web application. This class is known as the "ShellCommand" class, because it extends the SensorShell with a set of "commands" for this SDT. Example 15.4, “The org.hackystat.doc.simplesdt.SimpleSdtShellCommand class” illustrates the ShellCommand class for SimpleSdt, again with import statements and JavaDocs ommitted for space reasons.

Example 15.4. The org.hackystat.doc.simplesdt.SimpleSdtShellCommand class

package org.hackystat.doc.simplesdt;

public class SimpleSdtShellCommand extends ShellCommandAdapter {

  public String getHelpString() {
    return
    "SimpleSdt#set#tool=<tool>" + cr +
    "  Sets the Tool attribute value used in subsequent SimpleSdt sensor shell commands." + cr +
    "SimpleSdt#add#<key1>=<value1>#<key2>=<value2>#..." + cr +
    "  <keyN> is: fileName, elapsedTime, [tool], [pMap]." + cr;
  }

  public boolean add(Date timestamp, Map keyValStringMap, SensorShell shell) {
    try {
      // Add the timestamp and tool attributes to the keymap. 
      keyValStringMap.put("tstamp", String.valueOf(timestamp.getTime()));
      if (!keyValStringMap.containsKey("tool")) {
        keyValStringMap.put("tool", this.tool);
      }
      // Check that we can create a valid SimpleSdt, then add it to the list of data to be sent off.
      SimpleSdt entry = (SimpleSdt) SensorDataEntryFactory.getEntry("SimpleSdt", keyValStringMap);
      this.entryList.add(entry);
      // Record this entry for potential offline data storage.
      OfflineManager.getInstance().add(timestamp, "SimpleSdt", "add", keyValStringMap);
      this.resultMessage = "SimpleSdt add OK (" + entryList.size() + " total)";
    }
    catch (Exception e) {
      this.resultMessage = "SimpleSdt add error:" + cr + StackTrace.toString(e); 
      return false;
    }
    return true;
  }
}

The above ShellCommand implementation probably looks a bit confusing. To help understand the code, the next section provides a brief introduction to the Hackystat SensorShell, the underlying infrastructure that is extended by SDT ShellCommands.

15.7.1. The SensorShell and Shell Commmands

The SensorShell serves as a kind of client-side "middleware" to sensors that provides them with a variety of important services associated with sensor data transmission. For example, sensors do not need to implement any Soap transmission code; the SensorShell takes care of those details. Even more usefully, the SensorShell will automatically cache sensor data when a network connection cannot be made to the server (for example, when the developer is working offline). The sensor data will be accumulated and sent whenever the developer subsequently is back online and invokes a tool with a sensor. The idea is that sensor code can focus on the details of extracting process and product information from the tool, then leave the details of data encoding and transmission of that information to the SensorShell. (For more details, see Chapter 16, The SensorShell. This section is intended to provide just enough information to provide a context for the ShellCommand class.)

To simplify the life of sensor code developers as much as possible, it is useful for the SensorShell to provide an interface that is "smart" about the set of SensorDataTypes present in the system. To support this, the SensorShell is designed to allow an extensible "command" language, where each Sensor Data Type can define as many commands as it needs to support effective data collection by the sensors. The purpose of the ShellCommand class is to specify the set of commands specific to the processing of sensor data for a single SDT.

Example 15.5, “SensorShell Interactive Session” illustrates a short interactive session with the SensorShell that illustrates how it is extended with SDT-specific commands in general, as well as how the methods in the SimpleSdt ShellCommand class are used. New lines and bold font have been inserted to help distinguish user input from SensorShell output.

Example 15.5. SensorShell Interactive Session

C:\svn\hackyCore_Build\build\war\download>java -jar sensorshell.jar
Hackystat Version: 7.3.203 (February 3 2006 09:59:12)
SensorShell started at: 02/03/2006 11:38:08
Type 'help' for a list of commands.
Host: http://hackystat.ics.hawaii.edu/ is available and key is valid.
Defined shell command: SimpleSdt
Defined shell command: EvolSdt
AutoSend enabled every 10 minutes.
Checking for offline data to recover.
No offline data found.

>> help
SensorShell Command Summary
help
  This message.
quit
  Exit this sensor shell and send all accumulated data.
send
  Send all accumulated data.
Ping
  Attempts to contact the hackystat server and indicates success.
AutoSend#<number>
  Sets the number of minutes between background sending of any sensor data.
  Setting <number> to 0 disables autosending.
  The HACKYSTAT_AUTOSEND_INTERVAL property in sensor.properties can set AutoSend.
EvolSdt#set#tool=<tool>
  Sets the Tool attribute value used in subsequent EvolSdt sensor shell commands.
EvolSdt#add#<key1>=<value1>#<key2>=<value2>#...
  <keyN> is: name, elapsedTime, [tool], [pMap].
SimpleSdt#set#tool=<tool>
  Sets the Tool attribute value used in subsequent SimpleSdt sensor shell commands.
SimpleSdt#add#<key1>=<value1>#<key2>=<value2>#...
  <keyN> is: fileName, elapsedTime, [tool], [pMap].


>> SimpleSdt#add#fileName=c:\foo\bar\Zob.c#elapsedTime=25#tool=SensorShell
SimpleSdt add OK (1 total)


>> quit
Sending sensor data (02/03 11:48:08)
  Ping: Ping OK (contacted server http://hackystat.ics.hawaii.edu/ with valid key.)
  AutoSend: AutoSend OK ('send' command ignored)
  EvolSdt: Send OK (No entries to send.)
  SimpleSdt: Send OK (1 entries)

The example output is divided into four sections corresponding to the output returned from each of the four lines of user input. The first section results from the invocation of the SensorShell in interactive mode from the command line. The user enters "java -jar sensorshell.jar" to invoke the SensorShell interactively. When the SensorShell starts up, it prints version and build information, then reads in data from the user's sensor.properties file (configured via HackyInstaller) to determine the user's key and their Hackystat server (host). The SensorShell attempts to make a connection to the user's hackystat host immediately, and indicates whether it succeeded and whether the key it found is valid. It also indicates the set of sensor data types that it was built with (in this case, there are only two---SimpleSdt and EvolSdt), and other configuration information.

Once the initial startup processing is completed, the SensorShell enters a command line loop. In the example, the first command entered is "help", to which the SensorShell responds by printing out all of the available commands. There are five "built-in" commands in SensorShell: "help", "quit", "send", "Ping", and "AutoSend". Both the EvolSdt and the SimpleSdt SDTs contribute two additional commands: "set" and "add".

The second command entered in the example is "SimpleSdt#add#fileName=c:\foo\bar\Zob.c#elapsedTime=25#tool=SensorShell". This command has three components, separated by the first two occurrence of the "#" character. The first component is the command name, which in the case of commands defined by an SDT is always the name of the SDT itself. When the command is defined by an SDT, the second component specifies the name of the method in the ShellCommand class associated with the SDT that will process the command. The final component is a set of key-value pairs, also separated by the "#" character. Thus, the command is parsed as "SimpleSdt" (command), "add" (method to invoke), and "fileName=c:\foo\bar\Zob.c#elapsedTime=25#tool=SensorShell" (key-value pairs to provide to the add method).

One of the services provided by SensorShell is the buffering of sensor data at the client. It would be inefficient to send each instance of sensor data to the server as soon as it is generated, since the costs of establishing a new HTTP connection each time can easily increase the overhead by an order of magnitude or more. Instead, the SensorShell buffers the sensor data it receives for a certain period of time before sending it. This time interval is configurable using HackyInstaller, and defaults to 10 minutes. Of course, the SensorShell sends any unsent data when it exits. This is what happens in the final command illustrated in this example: the user enters the "quit" command, and the sensorshell invokes the "send()" command associated with each of its commands. The resulting output indicates that the sensor data instance added with the "add" command has now been sent to the server.

In addition to the interactive interface, SensorShell also has a programmatic API in Java. Sensors written in Java will simply create instances of SensorShell and invoke its public methods to send it data. However, sensors not written in Java will normally create a subprocess that runs the interactive version of SensorShell and send strings to that process. This is how, for example, the Emacs sensor is implemented.

Now that you've had this quick overview of SensorShell, let's return to the discussion of the SimpleSdt ShellCommand class implementation.

15.7.2. The getHelpString method

As shown in the example, the SensorShell's "help" command must print out information about all of its supported commands. To do this, it first prints out information about its built-in commands, then iterates through the ShellCommand class instances it has instantiated on startup and invokes their getHelpString method to obtain a String containing documentation about the commands supported by this ShellCommand. In the case of SimpleSdt, the getHelpString method looks like this:

  public String getHelpString() {
    return
    "SimpleSdt#set#tool=<tool>" + cr +
    "  Sets the Tool attribute value used in subsequent SimpleSdt sensor shell commands." + cr +
    "SimpleSdt#add#<key1>=<value1>#<key2>=<value2>#..." + cr +
    "  <keyN> is: fileName, elapsedTime, [tool], [pMap]." + cr;
  }

As you can see, this method returns a String that is included as part of the output of the "help" command in the example.

15.7.3. The add method

By convention, most SDT ShellCommand classes include a method called "add", which is invoked by sensor code to provide a new sensor data instance to the SensorShell. The method could be named anything at all, and a ShellCommand class might provide several different methods which all provide variations on the adding of data, but in most cases a single "add" method works well. The add method implemented by SimpleSdt is illustrated below:

  public boolean add(Date timestamp, Map keyValStringMap, SensorShell shell) {
    try {
      // Add the timestamp and tool attributes to the keymap. 
      keyValStringMap.put("tstamp", String.valueOf(timestamp.getTime()));
      if (!keyValStringMap.containsKey("tool")) {
        keyValStringMap.put("tool", this.tool);
      }
      // Check that we can create a valid SimpleSdt, then add it to the list of data to be sent off.
      SimpleSdt entry = (SimpleSdt) SensorDataEntryFactory.getEntry("SimpleSdt", keyValStringMap);
      this.entryList.add(entry);
      // Record this entry for potential offline data storage.
      OfflineManager.getInstance().add(timestamp, "SimpleSdt", "add", keyValStringMap);
      this.resultMessage = "SimpleSdt add OK (" + entryList.size() + " total)";
    }
    catch (Exception e) {
      this.resultMessage = "SimpleSdt add error:" + cr + StackTrace.toString(e); 
      return false;
    }
    return true;
  }

For a method in the ShellCommand class to be invokable as part of command processing, it must be a public method that returns a boolean and which accepts exactly three arguments: a Date, a Map, and a SensorShell. In this case, the SensorShell argument is not needed by the add method, but it must still be provided so that the method can be invoked correctly by reflection.

The Date instance is a timestamp that indicates when the sensor data was generated. In interactive mode, the Date timestamp is generated automatically at the moment that the command is entered. The Map is an instance initialized by parsing the key-value pairs on the command line that were delimited by the "#" character. Finally, the SensorShell is an instance of the SensorShell class that this method should use to support its processing.

In this implementation of the add command, the first step is to take the passed timestamp and add it to the keyValStringMap as the value of the "tstamp" entryattribute. As you hopefully recall, this is one of the implicitly defined entryattributes that must be associated with every SDT and which forms a unique key for sensor data instances of a given type.

The second step is to check to see whether the passed key-value map already contains an entry for the "tool" entryattribute. If not, it provides a value in the form of the this.tool instance variable. (This instance variable will be discussed further below.)

The third step is to do a sanity check on the collected set of key-value pairs by creating an instance of SimpleSdt using the SensorDataEntryFactory.getEntry method. This does the processing required to eventually create an instance of the SimpleSdt wrapper class, calling the getErrors() method along the way to ensure that the entryattributes are filled out correctly. If converters or defaulters fail, exceptions might be thrown at this point.

If the instance of the SimpleSdt was created without error, then we know we have a valid instance of sensor data. We can now add it to a local instance variable called this.entryList, and also provide details about this entry to the "OfflineManager" singleton class instance, which is responsible for caching data locally if a network connection is not available when an attempt to send the data is eventually made.

If all goes well, then the final action is to set the local instance variable this.resultMessage to a string indicating that the add method completed processing successfully. The method returns true in this case.

If all does not go well, and an exception was generated somewhere along the way, then the catch clause executes, which sets the local instance variable this.resultMessage to a different string indicating the error, and false is returned as the value of this method.

While this does indeed explain the add method, it raises new questions, such as where exactly did these local instance variables come from? The next section resolves this and other implementation mysteries by discussing the ShellCommandAdapter class.

15.7.4. The ShellCommandAdapter helper class

The alert reader will have noticed some irregularities in the above example of SensorShell and the SimpleSdt ShellCommand implementation. For example, the help string indicates that there should be a "set" method that enables one to specify the "tool" entryattribute, but that method is not found in the ShellCommand class. In addition, upon exiting the SensorShell, a "send" method for SimpleSdt should be invoked, but again that method is not present. Finally, the SimpleSdt ShellCommand class makes reference to several mysterious instance variables, including "this.tool", "this.entryList", and "this.resultMessage".

It turns out that early in the development of SensorShell, we noticed that most of our ShellCommand classes wanted to implement many of the same kinds of things: they wanted, for example, to have the sensor provide the "tool" entryattribute just once, not redundantly with each add command. They wanted a way to store the entries until their associated "send" method was invoked, and so forth. To reduce the redundancy and make most ShellCommand classes smaller and simpler, we created the ShellCommandAdapter abstract class, which is a subclass of ShellCommand and which provides these facilities.

A ShellCommand class is not obligated to extend ShellCommandAdapter; it could simply implement ShellCommand and provide the required methods from scratch. However, virtually all of the ShellCommand classes use ShellCommandAdapter to simplify their implementation.

15.7.5. Summary

This section illustrated the implementation of the ShellCommand class, which provides the bridge from the Sensor Data Type to the Hackystat infrastructure for sensor data transmission called SensorShell. Once you've implemented this class, it is time to put all of the pieces together and see if they work. We start this process in the next section, which discusses the development of the unit tests for SimpleSdt.