| Click here to support our advertisers

SHOPPING
JOB BANK
CLASSIFIEDS
DIRECTORIES
REFERENCE Online Library
LEARNING CENTER
JOURNAL
NEWS CENTRAL
DOWNLOADS
COMMUNITY
CALENDAR
ABOUT US
Journal:
Get the weekly email highlights from the most popular journal for developers! Current issue
> developer.com developerdirect.com htmlgoodies.com javagoodies.com jars.com intranetjournal.com javascripts.com | All Categories : Java # Chapter 10 # Native Methods and Java ### CONTENTS Deciding to Use Native Methods Native Methods from the Java Side Writing Native Methods Using Javah Java Arrays The Stubs Code Chapter Project: A Database Interface Library Using ODBC Calling Back Into Java Constructing Java Objects from C Creating the Library Database Server Adding Packet Assembly to DGTP The Election Server Election Client Summary This chapter builds on the concepts of Chapter 9, "Java Socket Programming," to construct a sophisticated database server. The server reads from a back-end database in real time. Since the standard Java classes do not, as yet, provide a database interface, native methods must be used to read the tables. A native interface library is developed and used to read a sample database. Along the way, you will learn the following: Calling C methods from Java Creating C libraries that can be called from Java Handling Java types as arguments to C functions Accessing Java class members from C functions Calling Java methods from C functions Throwing Java exceptions from a C function ## Deciding to Use Native Methods The decision to use native methods comes with a heavy cost. Any classes that load native methods cannot be used in an applet! There aren't any browsers available that allow applets to call native methods. The reason for this restriction is security. The Java security manager can't protect against malicious attacks from within a native method. The only solution is to not allow native methods to be called. Another disadvantage is the lost portability of your applications. One of the chief benefits of using Java is the portability of the resulting code between disparate platforms. A small industry has developed trying to provide truly portable application frameworks. For all their refinement, you are still left recompiling a version for each platform. Java steps into the fray with an intermediate format that enables you to compile once and execute everywhere. When you choose to use native methods, you lose this capability. Once again, you will be relegated to coding a separate library for each platform that runs Java. Now that the downside to native methods is clear, why use them at all? The single best reason to resort to native methods is to add functions not present in the standard classes. Maybe you want to interface with a specific piece of hardware or use a new network driver. Whatever the reason, native methods supply the capability. Because Java is portable, it cannot take advantage of operating specific features. The Java developers endeavored to supply the standard classes with all needed functionality, but this is an impossible task. The ability to call native C methods supplies a way to use features not available through the Java classes. Most of the functions in the standard classes themselves have to resort to native method calls to accomplish their tasks. ## Native Methods from the Java Side Native methods within a Java class are very simple. Any Java method can be transformed into a native method-simply delete the method body, add a semicolon at the end, and prefix the `native` keyword. The following Java method `public int myMethod(byte[] data) { ... }` becomes `public native int myMethod(byte[] data);` Where does the method body get implemented? In a Java-called native library that gets loaded into Java at runtime. The class of the above method would have to cause the library to be loaded. The best way to accomplish the load is to add a static initializer to the class: `static { System.loadLibrary("myMethodLibrary"); }` Static code blocks are executed once by the system when the class is first introduced. Any operations may be specified, but library loading is the most common use. If the static block fails, the class will not be loaded. This ensures that no native methods are executed without the underlying libraries. That's all there is to Java-side native methods. All the complexity is hidden within the native library. A native method appears to Java like all other real Java methods. In fact, all the Java modifiers (public, private, and so forth) apply to native methods as well. ## Writing Native Methods The Java runtime was implemented in the C programming language, so currently the only native language supported is C. The entry points into a C library that can be called from Java are called stubs. When you execute a native method call, a stub is entered. Java tries to ease the transition into native code by supplying a tool to generate a C header file and stub module.
> Note
> Any language that can link with and be called by C can be used to implement a native method. The C language is needed only to provide the actual interface with Java. Any additional, non-Java processing could be done in another language, such as Pascal.
> ### Using `Javah` `Javah` is the tool used to generate C files for Java classes; here's how you use it: `javah [options] class` Table 10.1 briefly lists the options available. By default, `javah` will create a C header (.h) file in the current directory for each class listed on the command line. Class names are specified without the trailing `.class`. Therefore, to generate the header for SomeName.class, use the following command: `javah SomeName` Table 10.1. `Javah` options.
> Option | Description
> `-verbose` | Causes progress strings to be sent to stdout
> `-version` | Prints the version of javah
> `-o outputfile` | Overrides default file creation;uses only this file
> `-d directory` | Overrides placement of output in current directory
> `-td tempdirectory` | Overrides default temp directory use
> `-stubs` | Creates C code module instead of header module
> `-classpath path` | Overrides default classpath
> Note
> If the class you want is within a package, then the package name must be specified along with the class name: `javah java.net.Socket`. In addition, `javah` will prefix the package name to the output filename: `java_net_Socket.h`.
> Listing 10.1 is a simple class with native methods. The class was chosen because it uses most of the Java types. Compile this class and pass it to `javah`. Listing 10.1. A simple class using native methods. `public class Demonstration { public String publicName; private String privateName; public static String publicStaticName; private static String privateStatucName; public native void method1(); public native int method2(boolean b, byte by, char c, short s); public native byte[] method3(byte data[], boolean b[]); public native String[] method4(int num, long l, float f, double d); static { System.loadLibrary("Demonstration"); } }` The `javah` output of the above class is in Listing 10.2. Listing 10.2. `Javah` output header of Demonstration class. `/* DO NOT EDIT THIS FILE - it is machine generated */ #include <native.h> /* Header for class Demonstration */ #ifndef _Included_Demonstration #define _Included_Demonstration struct Hjava_lang_String; typedef struct ClassDemonstration { struct Hjava_lang_String *publicName; struct Hjava_lang_String *privateName; /* Inaccessible static: publicStaticName */ /* Inaccessible static: privateStatucName */ } ClassDemonstration; HandleTo(Demonstration); #ifdef __cplusplus extern "C" { #endif __declspec(dllexport) void Demonstration_method1(struct HDemonstration *); __declspec(dllexport) long Demonstration_method2(struct HDemonstration *,/*boolean*/ long, char,unicode,short); __declspec(dllexport) HArrayOfByte *Demonstration_method3(struct HDemonstration *, HArrayOfByte *,HArrayOfInt *); __declspec(dllexport) HArrayOfString *Demonstration_method4(struct HDemonstration *,long, int64_t,float,double); #ifdef __cplusplus } #endif #endif` The class has been transformed into a C structure. Each class member is represented, except for static fields. Representation in a structure has an interesting side effect. Native methods have access to all non-static fields, including private class members. You are free to read and alter any member of the class. Now focus your attention on the four native method prototypes. Each method has been renamed by prefixing the class name to the method name. Had this class been contained in a package, the package name also would have been added. Each method has an additional argument. All native methods have a `this` pointer that allows the function to access the variables of its associated class. This argument is often referred to as an "automatic" parameter. Java will add this parameter to your methods automatically. The final piece to the puzzle is the `HandleTo()` macro. Every object in Java is represented in a structure called a JHandle. The format of this structure for the Demonstration class is as follows: `struct HDemonstration { ClassDemonstration *obj; methodtable *methods; }` The `HandleTo()` macro names the JHandle by adding an H to the passed name. To access any member of a JHandle class, you must dereference it with the `unhand()` macro. This macro has the opposite effect of `HandleTo()`. The following line retrieves a string member from the Demonstration class: `Hjava_lang_String str = unhand(demoPtr)->publicName;` The code for the `unhand()` macro shows the conversion: `#define unhand(o) ((o)->obj)` Structure member `obj` is obviously the class structure, but what of the other structure member? Typically, structure member methods will contain a pointer to an internal Java runtime structure that represents all the information on a class. This includes the Java byte codes, the exception table, any defined constants, and the parent class. There are times, however, when the variable is not a pointer at all. Java has reserved the lower 5 bits of the pointer for flags. If all 5 bits are zero, then the value is a pointer. If the lower 5 bits are non-zero, then the methods field becomes a typecode. You will encounter typecodes whenever you handle arrays. ### Java Arrays Arrays are handled uniquely in Java. They are considered objects, though they have no methods. Arrays occupy the realm somewhere between basic runtime types, such as `int` or `long`, and formal class objects. In Java, basic types are represented in a compact form. It would be inefficient to have all the class baggage carried around with something simple like an integer. When you need to represent an `int` as an object, you use a wrapper class, such as class Integer. This is why the "wrapper" classes are necessary. Arrays are much more complicated than numbers because they have variable length and multiple members. Like class objects, their storage is best represented by a C structure. Unlike class objects, arrays don't have methods. It was decided that the `methodtable` pointer could be better used as a scalar quantity for arrays. The upper 27 bits of the pointer represent the length of the array, and the lower 5 bits represent the type of data the array contains. All the runtime types are actually represented in the lower 4 bits. The fifth bit is reserved for compiler usage. Table 10.2 shows the encoding of the lower 4 flag bits and their meanings. Table 10.2. Type encoding.
> Encoding | Type
> 0000 | T_NORMAL_OBJECT
> 0001 | Unused
> 0010 | T_CLASS
> 0011 | Unused
> 0100 | T_BOOLEAN
> 0101 | T_CHAR
> 0110 | T_FLOAT
> 0111 | T_DOUBLE
> 1000 | T_BYTE
> 1001 | T_SHORT
> 1010 | T_INTEGER
> 1011 | T_LONG
> There are macros to help you read both the type bits and the length. The `obj_flags()` macro will return the flag bits, and `obj_length()` will return the array length. Both must be passed a JHandle pointer: `if ( obj_flags( demoPtr ) != T_NORMAL_OBJECT ) length = obj_length( demoPtr );` In practice, you will not need to check the type bits because Java will create and pass one of the standard array structures. You can see this in `Demonstration_method3()`. The parameter `byte[]` has been passed as an HArrayOfByte pointer. All the standard array structures have a single member: `body[1]`. Table 10.3 lists all the array structures and their contents. To access an array member, dereference the JHandle and index into the body array. The following line reads the fifth byte from a Java byte array: `char fifthByte = unhand(hByte)->body[4];` As you can see, Java arrays are zero-based just like C arrays. Table 10.3. Standard array structures.
> Structure | Contents
> ArrayOfByte | char body[1]
> ArrayofChar | unicode body[1]
> ArrayOfShort | signed short body[1]
> ArrayOfInt | long body[1]
> ArrayOfLong | int64_t body[1]
> ArrayOfFloat | float body[1]
> ArrayOfDouble | double body[1]
> ArrayOfArray | JHandle *(body[1])
> ArrayOfObject | HObject *(body[1])
> ArrayOfString | HString *(body[1])
> In contrast to JHandle and array pointers, the Java basic types are passed and referenced as direct C types. Table 10.4 displays all the basic Java types and their corresponding C representation. Table 10.4. C representation of Java basic types in Windows 95.
> Java Type | C Representation
> `boolean` | long
> `char` | unicode
> `short` | short
> `int` | long
> `long` | int64_t
> `float` | float
> `double` | double
> Note
> All the type information in this chapter is specific to Windows 95. The Java type representations may be different on another platform. It is best to run `javah` on the Demonstration class to verify the C representations when working on a different platform. In addition, all the macros and structures discussed can be found in the Java header files in the Java/include directory. These files are specific to a given platform and, as such, should be consulted when performing native method work.
> Now that you understand the C side of Java data, it's time to generate the code that interfaces Java to C. ### The Stubs Code Run the following command on the Demonstration class: `javah -stubs Demonstration` The output will be a C file in the current directory. Listing 10.3 shows the result. Listing 10.3. The output of `javah -stubs Demonstration`. `/* DO NOT EDIT THIS FILE - it is machine generated */ #include <StubPreamble.h> /* Stubs for class Demonstration */ /* SYMBOL: "Demonstration/method1()V", Java_Demonstration_method1_stub */ declspec(dllexport) stack_item * Java_Demonstration_method1_stub(stack_item *_P_,struct execenv *_EE_) { extern void Demonstration_method1(void *); (void) Demonstration_method1(_P_[0].p); return _P_; } /* SYMBOL: "Demonstration/method2(ZBCS)I", Java_Demonstration_method2_stub */ declspec(dllexport) stack_item * Java_Demonstration_method2_stub(stack_item *_P_,struct execenv *_EE_) { extern long Demonstration_method2(void *,long,long,long,long); _P_[0].i = Demonstration_method2(_P_[0].p,((_P_[1].i)), ((_P_[2].i)),((_P_[3].i)), ((_P_[4].i))); return _P_ + 1; } /* SYMBOL: "Demonstration/method3([B[Z)[B", Java_Demonstration_method3_stub */ declspec(dllexport) stack_item * Java_Demonstration_method3_stub(stack_item *_P_,struct execenv *_EE_) { extern long Demonstration_method3(void *,void *,void *); _P_[0].i = Demonstration_method3(_P_[0].p,((_P_[1].p)),((_P_[2].p))); return _P_ + 1; } /* SYMBOL: "Demonstration/method4(IJFD)[Ljava/lang/String;", Java_Demonstration_method4_stub */ declspec(dllexport) stack_item * Java_Demonstration_method4_stub(stack_item *_P_,struct execenv *_EE_) { Java8 _t2; Java8 _t4; extern long Demonstration_method4(void *,long,int64_t,float,double); _P_[0].i = Demonstration_method4(_P_[0].p, ((_P_[1].i)), GET_INT64(_t2, _P_+2), ((_P_[4].f)), GET_DOUBLE(_t5, _P_+5)); return _P_ + 1; }` This file contains the stub functions for each of the four native methods. It is the stub's job to translate Java data structures into a C format. Once this is done, the stub will then enter your C function. Sometimes the stub will have to do a little extra work to make the transition. For example, take a look at `method4`'s stub. The Java stack is made up of 32-bit words. Java data types `long` and `double` each command 64 bits of storage. The stub code calls "helper" functions to extract the data from the Java stack. The stubs will perform all the work necessary, no matter how complex, to interface the Java stack to C. The other interesting feature of the stub module is the `SYMBOL` comment at the top of each method. Java uses a system of method "signatures" to identify functions. The signature contains the method arguments and the return type; the symbols are explained in Table 10.5. Table 10.5. Method signature symbols.
> Type | Signature Character
> `byte` | B
> `char` | C
> `class` | L
> `end of class` | ;
> `float` | F
> `double` | D
> `function` | (
> `end of function` | )
> `int` | I
> `long` | J
> `short` | S
> `void` | V
> `boolean` | Z
> Signatures are important because they enable you to make calls back into the Java system. If you know the class, name, and signature of a method, then these elements can be used to invoke the Java method from within a C library. The format of a signature is as follows: `"package_name/class_name/method_name(args*)return_type"` Arguments can be any combination of the characters in Table 10.5. Class name arguments are written like this: `Lclass_name;` The semicolon signals the end of the class name, just as the right (closing) parenthesis signals the end of an argument list. Arrays are followed by the array type: `[B for an array of bytes [Ljava/langString; for an array of objects (in this case, Strings)` The Demonstration class is not actually going to be used; it's merely a convenient tool to demonstrate the C features of Java's runtime environment. Now it's time to move on to the chapter project and some actual native method code. ## Chapter Project: A Database Interface Library Using ODBC The goal of this project is to be able to read from a database. Although this project uses ODBC for its database access layer, any embedded SQL routines could be used. The database query routine has been separated from the Java return logic within the native method. Any database access method could easily be substituted. In fact, if you don't have ODBC installed, a synthetic query routine is supplied on the CD-ROM in the file FakeDatabaseImpl.c. The project will consist of two classes and an interface library. A container class will be used to house the query statement and resulting data, and a second class called Database will perform all the native methods. Listing 10.4 lays out the SQLStmt class. The native method library reads and writes directly to the variables in this container class. Listing 10.4. The SQLStmt class. `import java.io.*; import java.lang.*; import DBException; /** * Class to contain an SQL statement and resulting data */ public class SQLStmt { // The query string public String sqlStmt = null; // The actual data from the query private String result[][]; private int nRows, nCols; // True if the query is successful private boolean query = false; /** * The lone constructor, you must supply a query * string to use this constructor * @param stmt contains the query to execute */ SQLStmt(String stmt) { sqlStmt = stmt; System.out.println("Statement: " + stmt); } /** * Return the number of rows in a query data set * @exception DBException if no query has been made */ public int numRows() throws DBException { if ( !query ) throw new DBException("No active query"); return nRows; } /** * Return the number of cols in a query data set * @exception DBException if no query has been made */ public int numCols() throws DBException { if ( !query ) throw new DBException("No active query"); return nCols; } /** * Retreive the contents of a row. Each column * is separated from the others by a pipe '|' character. * @param row is the row to retreive * @exception DBException if invalid row */ public String getRow(int row) throws DBException { if ( !query ) throw new DBException("No active query"); else if ( row >= nRows ) throw new DBException("Row out of bounds"); String buildResult = new String(""); for ( int x = 0; x < nCols; x++ ) buildResult += (result[row])[x] + " |"; return buildResult; } /** * Retreive the contents of a column. * @param row, col is the column to retreive * @exception DBException if invalid row or column */ public String getColumn(int row, int col) throws DBException { if ( !query ) throw new DBException("No active query"); else if ( row >= nRows ) throw new DBException("Row out of bounds"); else if ( col >= nCols ) throw new DBException("Column out of bounds"); return result[row][col]; } public void allDone(String str) { System.out.println(str); } /** * Display the contents of the statement */ public String toString() { String s = new String(); if ( query == false ) { s += sqlStmt; } else { try { for ( int x = 0; x < nRows; x++ ) s += getRow(x) + "\n"; } catch (DBException de) { System.out.println(de); } } return s; } }` The SQLStmt class has public methods to enable extracting query data in an orderly manner: `numRows()`, `numCols()`, `getRow()`, and `getColumn()`. SQLStmts are two-way objects-they hold both the input and the output data. The output is contained in a two-dimensional array of Strings. This scheme forces the database interface library to translate all table columns into String format. A new exception has been defined with this class. It can be found on this book's CD-ROM in the file DBException.java. This exception is thrown by the SQLStmt class when a request is made, but no query has been attempted. The Database class, discussed in the following paragraphs, also throws DBExceptions. The method `allDone()` will be called by the native library and gives the library a convenient way to print. It serves no other purpose. Although the SQLStmt class contains all the database information, it does not actually interface with the native library. For this task, the Database class is used. This class is much simpler than the data container; its chief role is to interface with the native methods. Listing 10.5 shows the Database class. Listing 10.5. The Database class. `import java.lang.*; import java.io.*; import SQLStmt; import DBException; /** * Class to allow access to the database library */ public class Database { // Table name to use (ODBC data source) public String tableName; /** * Lone constructor. A data source must be passed. * @param s holds the name of the data source to use */ Database(String s) { tableName = s; } /** * Native method query * @param stmt holds the SQLStmt class to use * @exception DBException is thrown on any error */ public synchronized native void query(SQLStmt stmt) throws DBException; public synchronized native SQLStmt sql(String stmt) throws DBException; static { System.loadLibrary("Database"); } }` The first native method uses an SQLStmt object for both input and output, and the second native method uses a String input and returns an SQLStmt object as output. It is the native library's task to create and fill the output object. Both native methods are marked as synchronized because the library implementation is single-threaded. Nothing in Java precludes making re-entrant native libraries, but the database library uses global variables for storage. This makes it necessary to protect the library from being entered by more than one thread at a time. As with the Demonstration class, the first step is to compile the classes and pass them to the `javah` tool: `javac SQLStmt.java Database.java javah SQLStmt Database` Here is the output for the SQLStmt class: `/* DO NOT EDIT THIS FILE - it is machine generated */ #include <native.h> /* Header for class SQLStmt */ #ifndef _Included_SQLStmt #define _Included_SQLStmt struct Hjava_lang_String; typedef struct ClassSQLStmt { struct Hjava_lang_String *sqlStmt; struct HArrayOfArray *result; long nRows; long nCols; /*boolean*/ long query; } ClassSQLStmt; HandleTo(SQLStmt); #ifdef __cplusplus extern "C" { #endif #ifdef __cplusplus } #endif #endif` Notice how the two-dimensional array has been translated into HArrayOfArray. SQLStmt doesn't have any native methods, though the `javah` tool still places the surrounding `ifdef cplusplus` statements where native methods would normally appear. Here is the output for the Database class: `/* DO NOT EDIT THIS FILE - it is machine generated */ #include <native.h> /* Header for class Database */ #ifndef _Included_Database #define _Included_Database struct Hjava_lang_String; typedef struct ClassDatabase { struct Hjava_lang_String *tableName; } ClassDatabase; HandleTo(Database); #ifdef __cplusplus extern "C" { #endif struct HSQLStmt; __declspec(dllexport) void Database_query(struct HDatabase *,struct HSQLStmt *); __declspec(dllexport) struct HSQLStmt *Database_sql(struct HDatabase *,struct Hjava_lang_String *); #ifdef __cplusplus } #endif #endif` The two native methods appear at the bottom of the header. Since this file has native methods, it needs to have stub code generated for it. The next step is to execute the `javah` tool with the stubs option: `javah -stubs Database`
> Note
> There is no rule about what the stub module must be called. You can use the `-ofilename` option to override `javah`'s default naming convention. This option is also useful for forcing the output from multiple classes into a single stubs file: `javah -stubs -ostubs.c class1 class2 class3`. This can be done as long as all the native methods for classes 1, 2, and 3 will appear in the same native library.
> Here is the stub module for the Database class: `/* DO NOT EDIT THIS FILE - it is machine generated */ #include <StubPreamble.h> /* Stubs for class Database */ /* SYMBOL: "Database/query(LSQLStmt;)V", Java_Database_query_stub */ declspec(dllexport) stack_item *Java_Database_query_stub(stack_item *_P_, struct execenv *_EE_) { extern void Database_query(void *,void *); (void) Database_query(_P_[0].p,((_P_[1].p))); return _P_; } /* SYMBOL: "Database/sql(Ljava/lang/String;)LSQLStmt;", Java_Database_sql_stub */ declspec(dllexport) stack_item *Java_Database_sql_stub(stack_item *_P_, struct execenv *_EE_) { extern long Database_sql(void *,void *); _P_[0].i = Database_sql(_P_[0].p,((_P_[1].p))); return _P_ + 1; }` All that's left to do is to write the implementation module for the Database stub. In the interest of having a comprehensible project layout, the name of this module will be DatabaseImpl.c. Listing 10.6 shows only the code that manipulates the Java structures. The entire source code for DatabaseImpl, including the ODBC calls, can be found on the CD-ROM. Listing 10.6. Java interface functions from DatabaseImpl.c. `#include <StubPreamble.h> #include <javaString.h> #include "Database.h" #include "SQLStmt.h" #include <stdio.h> #include <sql.h> #define MAX_WIDTH 50 #define MAX_COLS 20 #define MAX_ROWS 200 static SQLSMALLINT nRows, nCols; static SQLINTEGER namelen[ MAX_COLS ]; static char *cols[ MAX_COLS ]; static char *rows[ MAX_ROWS ][ MAX_COLS ]; bool_t throwDBError(char *description) { SignalError(0, "DBException", description); return FALSE; } /* * Extract from local storage into the passed String array. */ void getTableRow(struct HDatabase *db, HArrayOfString *result, long row) { int col; char *st; for ( col = 0; col < nCols; col++ ) { st = rows[row][col]; unhand(result)->body[col] = makeJavaString(st, strlen(st)); } } /* * Perform a database lookup using the passed HSQLStmt. */ void Database_query(struct HDatabase *db, struct HSQLStmt *stmt) { int x; HArrayOfArray *all; HString *s; /* Read from the database into local storage */ if ( doQuery(db, stmt) == FALSE ) freeStorage(); /* If we have data, store it in the class */ if ( nRows != 0 && nCols != 0 ) { /* Allocate the row array (1st dimension) */ all = (HArrayOfArray *)ArrayAlloc(T_CLASS, nRows); if ( !all ) { freeStorage(); unhand(stmt)->query = FALSE; throwDBError("Unable to allocate result array"); return; } /* Set the array into the HSQLStmt class object */ unhand(stmt)->result = all; /* For each row, store the result strings */ for ( x = 0; x < nRows; x++ ) { /* Allocate the columns (2nd dimension) */ all->obj->body[x] = ArrayAlloc(T_CLASS, nCols); /* Extract the data from local storage into the * HSQLStmt object. */ getTableRow(db, (HArrayOfString *)all->obj->body[x], x); } /* Set final variables in the object to reflect the query */ unhand(stmt)->query = TRUE; unhand(stmt)->nRows = nRows; unhand(stmt)->nCols = nCols; /* Print the results of the query by calling * allDone( HSQLStmt.toString() ); */ s = (HString *)execute_java_dynamic_method(0, (HObject *)stmt, "toString", "()Ljava/lang/String;"); execute_java_dynamic_method(0, (HObject *)stmt, "allDone", "(Ljava/lang/String;)V", s); } else unhand(stmt)->query = FALSE; } /* * Create a HSQLStmt class object and pass it to the * query routine. */ struct HSQLStmt *Database_sql(struct HDatabase *db, struct Hjava_lang_String *s) { HObject *ret; /* Create the object by calling its constructor */ ret = execute_java_constructor(0, "SQLStmt", FindClass( 0, "SQLStmt", TRUE), "(Ljava/lang/String;)", s); if ( !ret ) return NULL; Database_query(db, (HSQLStmt *)ret); return (HSQLStmt *)ret; }` The `doQuery()` function merely uses ODBC to read a sample database into the local two-dimensional array: rows. The storage for the array is allocated as needed, though the total possible size is limited by the constants `MAX_ROWS` and `MAX_COLS`. The `doQuery()` function also fills in the variables `nRows` and `nCols` to reflect the storage allocated in the local array. Once the data has been extracted from the database, it's time to move it into the SQLStmt container object. The native method `Database_query()` performs most of the interesting work. After making sure that `doQuery()` returns some data, the function first allocates a Java array to store the rows: `all = (HArrayOfArray *)ArrayAlloc(T_CLASS, nRows);` Unlike C, Java two-dimensional arrays are not allocated together. In Java, a two-dimensional array is actually an array of an array. The Java function `ArrayAlloc()` is used to make an array. The first parameter is the type of data that the array will contain; the second parameter is the array length. A JHandle pointer is returned. Since this is the first dimension of a two-dimensional array, it will contain arrays. `T_CLASS` represents any object, including arrays. It signals that the array contains Jhandles.
> Note
> The rows of a two-dimensional Java array do not have to contain the same number of columns. Some rows could even be NULL. You should be aware of this when dealing with multidimensional arrays. The database library will make sure that the number of columns is consistent throughout the array.
> Assuming everything allocated successfully, the JHandle is placed into the SQLStmt class with the following line: `unhand(stmt)->result = all;` If there is an error, the native method will throw a DBException by using the function `throwDBError()`. A Java function called `SignalError()` is used to throw the actual exception: `SignalError(0, "DBException", description);` The first parameter is a structure called `execenv`. Zero is substituted to cause the current environment to be used. The next parameter is the name of the exception, and the final parameter is the exception description. The preceding code line is equivalent to the Java line: `throw new DBException(description);`
> Note
> Whenever the `execenv` structure (or `ExecEnv`) is called for, you may substitute NULL or 0. This causes the Java runtime to use the current environment. The actual environment pointer is supplied to the stub methods as parameter `_EE_`, but it is not passed into the native implementations.
> An additional array is allocated for each row; this second dimension array has `nCols` members. The array will contain String objects, so `T_CLASS` is again passed into `ArrayAlloc()`. The created array is passed into `getTableRow()` to be filled in with the table data. The `getTableRow()` function creates a Java String object for each column's data: `unhand(result)->body[col] = makeJavaString(st, strlen(st));` The Java function `makeJavaString()` takes a C `char` pointer and the string length as parameters. It returns a JHandle to an equivalent Java String, then the created String is stored. There is a corollary function for `makeJavaString()`: `char *makeCString(Hjava_lang_String *s);` This function converts a Java String back into a C string. Storage for the string is allocated from the Java heap. You should keep the pointer in a Java class variable somewhere to prevent the C string from being garbage-collected. The following is an alternative method: `char *allocCString(Hjava_lang_String *s);` This function allocates the C string from the local heap by using `malloc()`. You are responsible for freeing the resulting pointer when you are finished with it. When all the rows have been created, the method tries to print the result. Originally, I printed the results from within Java, but I wanted to show you an example of C calling Java. The call was moved into the native library for this purpose. ### Calling Back Into Java Remember the discussion of method signatures? This is where they are used. Any Java method can be invoked from C: `s = (HString *)execute_java_dynamic_method(0, (HObject *)stmt, "toString", "()Ljava/lang/String;");` The function `execute_java_dynamic_method()` accomplishes the invocation. A JHandle to the object is passed, along with the method name and its signature. Without the correct signature, Java can't find the method to execute. The invoked method can return any normal Java value. In this case, a String was returned. Do you recognize the previous call? Its Java equivalent would be the following: `String s = stmt.toString();` There are actually three functions for calling back into Java: `Hobject *execute_java_constructor(ExecEnv *, char *classname, ClassClass *cb, char *signature, ...); long execute_java_statuc_method(ExecEnv *, ClassClass *db, char *method_name, char *signature, ...); long execute_java_dynamic_method(ExecEnv *, Hobject *, char *method_name, char *signature, ...);` You must know whether the method is static or dynamic because calling the wrong function will yield an exception. The ellipses at the end of each parameter list indicate a variable number of additional arguments, and the signature determines how many additional parameters there are. Structure ClassClass describes all the attributes of a Java class. You can find the `ClassClass` pointer from the JHandle structure. In addition, there is a Java "helper" function to find the ClassClass structure of a Java class: `ClassClass *FindClass(struct execenv *, char *name, bool_t resolve);` The second native method `Database_sql()` uses `FindClass()` to construct an instance of the SQLStmt class. ### Constructing Java Objects from C `Database_sql()` is passed a String object, but it needs to return an SQLStmt object. The return object must be created. Calling `execute_java_constructor()` will both create and initialize the desired object, but what is the signature of a constructor? The following routine will dump the contents of a class's method table: `void dumpMethodTable(ClassClass *cb) { int x; struct methodblock *mptr; fprintf(stderr, "There are %d methods in class %s\n", cb->methods_count, cb->name); mptr = cb->methods; for ( x = 0; x < cb->methods_count; x++, mptr++ ) { fprintf(stderr, "Method %02d: name: '%s' signature: '%s'\n", x, mptr->fb.name, mptr->fb.signature); } } void dumpMethods(HObject *han) { dumpMethodTable(han->methods->classdescriptor); }` Dumping SQLStmt's method table yields the following: `There are 7 methods in class SQLStmt Method 0: name: '<init>' signature: '(Ljava/lang/String;)V' Method 1: name: 'numRows' signature: '()I' Method 2: name: 'numCols' signature: '()I' Method 3: name: 'getRow' signature: '(I)Ljava/lang/String;' Method 4: name: 'getColumn' signature: '(II)Ljava/lang/String;' Method 5: name: 'allDone' signature: '(Ljava/lang/String;)V' Method 6: name: 'toString' signature: '()Ljava/lang/String;'` The signature of the constructor seems as though it should be `"(Ljava/lang/String;)V"`. This is not actually the case. Using this signature will yield an exception: `java.lang.NoSuchMethodError` The correct signature leaves off the trailing V.
> Note
> The signature of a constructor has NO return type at all. It should always be written as "(...)", not as "(...)V".
> After the constructor is run, the object is passed into the original query routine before it is returned to the caller. ### Creating the Library The final step is to compile the library. You must have Microsoft Visual C++ Version 2.x or above. The libraries on the CD-ROM were compiled with Visual C++ 4.0. To compile the database library, issue the following command: `cl Database.c DatabaseImpl.c -FeDatabase.dll -MD -LD odbc32.lib javai.lib` If you don't have ODBC32 installed in your system, a synthetic version can be made. Issue the following command to construct the synthetic version: `cl Database.c FakeDatabaseImpl.c -FeDatabase.dll -MD -LD javai.lib` Obviously, FakeDatabaseImpl.c makes no ODBC calls, but it does supply a simulated version of the data. Both of the above commands create the file Database.dll. The javai library provides access to the Java runtime DLL of the same name. It should be listed as the last library on the command line. The Database class still needs to be integrated into the server from Chapter 9, but the following Java application is suitable for testing the library itself. It can be found on the CD-ROM in the file TestDatabase.java. `import Database; /** * A simple test application for exercising the Database class. */ public class TestDatabase { public static void main(String args[]) { // Create the database class and assign the data source Database db = new Database("election.dbf"); // Make the a SQL statement to execute SQLStmt stmt = new SQLStmt("select * from election"); try { // Execute the 1st native method db.query(stmt); // Execute the 2nd native method db.sql("select * from election where State = 'Maryland'"); } catch (DBException de) { System.out.println(de); } } }`
> Note
> The database in this project has five columns: Candidate, State, Votes, % Precincts Reporting, and Electoral Votes. The database file, election.dbf, is in dBASE IV format. You will need to set up an ODBC data source called election.dbf to access this file.
Now that the database access class is written, it's time to integrate it with the client/server applet from the previous chapter. ## Database Server The project architecture consists of three threads. The main HTTP server thread forms the basis of the application and provides HTTP services. This main thread spawns a separate second thread, called ElectionServer, whose purpose is to perform additional server functions unrelated to HTTP. ElectionServer acts as a manager of the DGTP transmission protocol thread and provides access to the database. The DGTP thread is the third and final thread for the project-it's spawned and used by the election server. The main HTTP thread has no communication with either the election server or its DGTP thread. The client side mirrors the server. Instead of a HTTP server, the client substitutes a Java-enabled browser. The browser spawns the applet, and the applet spawns and manages the DGTPClient thread. Figure 10.1 illustrates the overall architecture. Figure 10.1 : The project architecture. This project uses the DGTP protocol from Chapter 9, but the protocol has a major limitation that must be addressed first. Currently, the amount of data being sent to a DGTP client must fit within the block size of the protocol (1024 bytes). This is not acceptable for a database server, so the protocol must be amended to serve arbitrarily large amounts of data. ## Adding Packet Assembly to DGTP Serving large data blocks isn't difficult, but it does require some overhead costs. DGTP will use a technique called chaining to send the data. Chaining simply means that large data will be sent as a series of smaller sub-blocks; each sub-block is marked to reflect both its position within a chain and the chain itself. In addition, the sub-blocks are marked as first-in-chain, middle-in-chain, or last-in-chain, depending on their chain position. Keep in mind that datagrams are still being used to send the sub-blocks, so it is quite possible for the sub-blocks to arrive at the receiver in a different order than they were sent. When a chained sub-block is received, it will have to be queued to a packet assembler to reconstruct the original chain. Only when all the sub-blocks are received can the data be forwarded. A packet assembler acts as a middle man-assembling packet fragments into a single continuous packet. Each transmission request is checked for size before it is sent. If the size will not fit into a single packet, the data block is forwarded to a separate send routine for chaining. This new routine will use a new DGTP command for its transmissions: `MDATA chain sub-block first-middle-or-last length` The new format for DGTP send operations is shown in Listing 10.7. Listing 10.7. New DGTP send operations. `/** * Send the block of data to the specified address. * @param dest contains the address to send to * @param data is the data to send * @param srcOffset is where to start sending from * @param length is the amount of data to send */ public void sendData(ClientAddr dest, byte[] data, int srcOffset, int length) { String hdr = new String("DGTP/" + DGTPver + " "); hdr += "DATA " + length + "\r\n\r\n"; if ( (hdr.length() + length) > PSIZE ) multiPartSend(dest, data, srcOffset, length); else { byte[] sendbuf = new byte[hdr.length() + length]; hdr.getBytes(0, hdr.length(), sendbuf, 0); System.arraycopy(data, srcOffset, sendbuf, hdr.length(), length); DatagramPacket sendPacket = new DatagramPacket( sendbuf, sendbuf.length, dest.address, dest.port); try { socket.send(sendPacket); } catch (IOException ioe) { System.out.println("IOException: Unable to send. " + ioe); } } } /** * Send the a large block of data to the specified address. * Large means bigger than the largest packet size (PSIZE). * @param dest contains the address to send to * @param data is the data to send * @param srcOffset is where to start sending from * @param length is the amount of data to send */ public void multiPartSend(ClientAddr dest, byte[] data, int srcOffset, int length) { int multiNum = chainNum++; int blockNum = 0; int sentSoFar = 0; while ( sentSoFar < length ) { String chain; String hdr = new String("DGTP/" + DGTPver + " "); hdr += "MDATA " + multiNum + " " + blockNum; // max = current header + sizeof(" xic ") + // sizeof("1024 ") + sizeof("\r\n\r\n"); int maxHdrSize = hdr.length() + 5 + MAX_PSIZE_STRING + 4; // Determine the biggest block we can send int blockLength = PSIZE - maxHdrSize; if ( blockLength <= 0 ) { System.out.println("Error: PSIZE is too small"); System.out.println("Header is " + maxHdrSize + " bytes long"); return; } // If block is more than we need, make it fit if ( (blockLength + sentSoFar) >= length ) { blockLength = length - sentSoFar; chain = " lic "; // last-in-chain } else if ( blockNum == 0 ) { chain = " fic "; // first-in-chain } else { chain = " mic "; // middle-in-chain } // finish wrting the header hdr += chain + blockLength + "\r\n\r\n"; byte[] sendbuf = new byte[hdr.length() + blockLength]; hdr.getBytes(0, hdr.length(), sendbuf, 0); System.arraycopy(data, srcOffset + sentSoFar, sendbuf, hdr.length(), blockLength); DatagramPacket sendPacket = new DatagramPacket( sendbuf, sendbuf.length, dest.address, dest.port); try { socket.send(sendPacket); } catch (IOException ioe) { System.out.println("IOException: Unable to send. " + ioe); return; } // Update counters blockNum++; sentSoFar += blockLength; } } }` The DGTPClient class also has to be changed to work with the new transmission scheme. Listing 10.8 shows only the changed methods. Listing 10.8. Changes to DGTPClient in support of chaining. `public DGTPClient(LiveDataNotify handler) { ... buildThread = new ClientPacketAssembler(this); ... } public void run() { DatagramPacket packet = null; try { regThread.start(); buildThread.start(); } ... } public void parsePacketData(DatagramPacket packet) throws IOException, ProtocolException { ... else if ( cmd.equals("MDATA") ) { handleNewMultiData(cmds, is); } ... } public void handleNewMultiData(StringTokenizer cmds, DataInputStream is) throws ProtocolException { int packetNum = Integer.valueOf(cmds.nextToken()).intValue(); int subBlockNum = Integer.valueOf(cmds.nextToken()).intValue(); boolean last = false; if ( cmds.nextToken().equals("lic") ) last = true; int length = Integer.valueOf(cmds.nextToken()).intValue(); byte[] data = new byte[length]; try { is.readFully(data); buildThread.newSubBlock(packetNum, subBlockNum, last, data); } catch (EOFException eof) { throw new ProtocolException( "Server packet too short: " + eof); } catch (IOException ioe) { throw new ProtocolException( "Error while reading server data: " + ioe); } }` The actual tracking and assembly of chains is done in the ClientPacketAssembly thread. The DGTPClient merely assembles the sub-block and its information, then passes the data into the assembly thread, shown in Listing 10.9. Listing 10.9. The ClientPacketAssembler class. `import java.lang.*; import java.util.*; import java.net.*; import java.io.*; import DGTPClient; /** * Packet assembler tracks and assembles multi part data blocks. */ public class ClientPacketAssembler extends Thread { private static final int TIMEOUT = 30000; // in milliseconds private static final int SLEEP_TIME = 5000; // in milliseconds DGTPClient ct = null; Hashtable partials = null; public ClientPacketAssembler(DGTPClient cthread) { ct = cthread; partials = new Hashtable(); } public void run() { while (true) { try { Thread.currentThread().sleep(SLEEP_TIME); } catch (InterruptedException ie) { System.out.println( "InterruptedException: in packet assembler thread: " + ie); } checkTimers(); } } /** * For each partial packet being tracked, decrement the timer * and kill the packet if it has expired. */ public synchronized void checkTimers() { for (Enumeration e = partials.elements(); e.hasMoreElements();) { PartialPacket pp = (PartialPacket)e.nextElement(); if ( pp.timer > 0 ) { pp.timer -= SLEEP_TIME; if ( pp.timer <= 0 ) { partials.remove( new Integer(pp.packetNum) ); ct.notifyCompleteBlock(null, true); } } } } /** * Add a new sub block. * @param packetNum contains the pnum being assembled * @param subBlockNum contains the bnum within this pnum * @param last is true if this is the last in a series * @param data contains the data for this sub block. */ public synchronized void newSubBlock(int packetNum, int subBlockNum, boolean last, byte data[]) { PartialPacket pp; pp = (PartialPacket)partials.get(new Integer(packetNum)); if ( pp == null ) { pp = new PartialPacket(packetNum); partials.put(new Integer(packetNum), pp); } pp.addSubBlock(subBlockNum, last, data); if ( pp.complete() ) { try { ct.notifyCompleteBlock(pp.getData(), false); partials.remove(new Integer(packetNum)); } catch (IOException ioe) { System.out.println("Error getting data: " + ioe); } } else pp.timer = TIMEOUT; } }` This class is structured to handle multiple chains simultaneously. A hash table is used to store each chain's assembly. The key to the hash table is the packet number, but it can't be passed directly to the hash table because `int` is a base type, not a class object. The `int` must first be placed in an Integer class wrapper. The ClientPacketAssembler class is a thread so that it can detect timeouts. The `run()` method checks for timeouts on each packet under assembly. Every time a sub-block is received, the sub-block's chain timer is reset. If the timer expires before a new sub-block arrives, the chain is killed. This protects the client if one of a chain's sub-blocks is lost. When a chain is killed, the DGTPClient is notified through the same function used to signal complete chains: `ct.notifyCompleteBlock(null, true);` The second parameter to the above function is a boolean error flag. If it is true, the first parameter is undefined. This flag is also added to the `recvNewData()` method within the LiveDataNotify interface: `public interface LiveDataNotify { public String getDestHost(); public int getDestPort(); public void recvNewData(byte[] newDataBlock, boolean error); public void connectRefused(); }` A new command has been added to the client to facilitate recovery from lost chains. In each application, you must decide what action to take if it loses a chain, but one option is to send a `REFRESH` request to the server. When a new sub-block arrives, the assembler checks to see whether a chain has been started. If no previous instance of the chain exists, a PartialPacket class, shown in Listing 10.10, is created to track the new chain. The current sub-block is then added to the chain. Listing 10.10. The PartialPacket tracking class. `/** * A private class for assembling packets */ class PartialPacket { public int timer; public int packetNum; private int totalBlocks; private int totalSize; private Hashtable blocks; private BitSet recvd; public PartialPacket(int pnum) { packetNum = pnum; timer = 0; totalBlocks = -1; totalSize = 0; blocks = new Hashtable(); recvd = new BitSet(); } /** * Handle a new sub block * @param subBlockNum is the strand being added * @param last is true if this is the last block * @param data contains the data for this strand */ public void addSubBlock(int subBlockNum, boolean last, byte data[]) { // Ignore duplicate packets, shouldn't occur if ( recvd.get(subBlockNum) ) return; // if last block, we can set the number of blocks // for this packet if ( last ) totalBlocks = subBlockNum + 1; totalSize += data.length; recvd.set(subBlockNum); blocks.put(new Integer(subBlockNum), data); } /** * Function to test whether a packet is completely * assembled. */ public boolean complete() { if ( totalBlocks != -1 ) { for ( int x = 0; x < totalBlocks; x++ ) { if ( recvd.get(x) == false ) return false; } return true; } return false; } /** * Assembles the strands into one big byte array. * @exception IOException if packet is not complete. */ public byte[] getData() throws IOException { byte ret[]; if ( complete() ) { ret = new byte[ totalSize ]; int bytesSoFar = 0; for ( int x = 0; x < totalBlocks; x++ ) { byte data[] = (byte[])blocks.remove(new Integer(x)); if ( data == null ) throw new IOException("Internal packet assembler error"); if ( data.length + bytesSoFar > totalSize ) throw new IOException("Internal packet assembler error"); System.arraycopy(data, 0, ret, bytesSoFar, data.length); bytesSoFar += data.length; } } else { throw new IOException("getData() of incomplete packet"); } return ret; } }` PartialPacket uses a hash table of its own to track each piece of a chain. The sub-block number is the key, and the data block is the value. A BitSet class is used to track each piece in relation to the whole. BitSets take up very little storage and operate like an array of booleans. When a sub-block comes in, its bit is set before it's added to the hash table. Once the last-in-chain sub-block appears, the object knows how many blocks are in the chain. The method `complete()` is called to test whether the chain has all its members. When all sub-blocks have been received, the chain is assembled by using method `getData()`. Only the DGTPServer can initiate MDATA blocks. The client is still limited to sending data that can travel within a single DGTP packet. ### The Election Server The NumUsersServer class from Chapter 9 will be used as the basis for the ElectionServer class. If you recall, NumUsersServer sent text commands to applets that connected to it. There was a single command: `CLIENTS num_of_connections` The ElectionServer adds two new commands: `RESULTS #rows #cols database_rows` `QUERY_RSP qnum #rows #cols database_rows` In addition, the ElectionServer will now have to handle incoming data blocks. The election client uses the `QUERY` command: `QUERY qnum query_string` Listing 10.11 shows the ElectionServer class. Listing 10.11. The ElectionServer class. `import java.lang.Thread; import java.net.DatagramPacket; import java.util.*; import DGTPServer; import LiveDataServer; import ClientAddr; public class ElectionServer extends Thread implements LiveDataServer { private static final int ONE_SECOND = 1000; private DGTPServer servThread = null; private Database election = null; private SQLStmt results = null; public ElectionServer(int hostPort) { servThread = new DGTPServer(this, hostPort); election = new Database("election.dbf"); try { results = election.sql("select * from election"); } catch (DBException de) { System.out.println("ERROR: " + de); System.out.println("Server exiting due to lack of data"); } } /** * Run method for this thread. * Issue a new query every 30 seconds. */ public void run() { boolean toggle = false; if ( results != null ) { servThread.start(); while(true) { sleep(30); try { SQLStmt nn; if (toggle) { nn = election.sql("select * from election"); toggle = false; } else { nn = election.sql("select * from election " + "where State = 'Maryland'"); toggle = true; } synchronized (results) { results = nn; } servThread.sendToUsers( formatResults("RESULTS", results)); } catch (DBException de) { System.out.println("Error: " + de); } } } } public boolean ValidateRegistrant(ClientAddr user) { return true; } /** * A private routine to concatenate the number of rows & cols * to a response string and then add the SQLStmt data. * If the query was invalid, send a NULL response. * * @param sql contains the data to return * @param resultType contains the initial String */ private String formatResults(String resultType, SQLStmt sql) { String ret = new String(resultType + " "); try { synchronized(sql) { ret += sql.numRows() + " " + sql.numCols() + " " + sql; } } catch (DBException de) { ret += "0 0"; } return ret; } /** * A new connection was accepted, send the latest data * @param user contains the address to send to */ public void NewRegistrant(ClientAddr user) { // broadcast the new user servThread.sendToUsers("CLIENTS " + servThread.Clients.size()); // send the latest data to only the new user servThread.sendData(user, formatResults("RESULTS", results)); } public void refreshRequest(ClientAddr user) { servThread.sendData(user, formatResults("RESULTS", results)); } /** * A connection was dropped. * @param user contains the address that was dropped */ public void DropRegistrant(ClientAddr user) { // broadcast the new number of users servThread.sendToUsers("CLIENTS " + servThread.Clients.size()); } /** * Recv data block routine * @param newDataBlock contains the data * @param who is the original recv packet */ public void recvNewData(byte[] newDataBlock, DatagramPacket who) { ClientAddr user = null; SQLStmt stmt = null; String query = new String(newDataBlock, 0); StringTokenizer cmds = new StringTokenizer(query, " \t"); if (cmds.nextToken().equals("QUERY")) { String qnum = cmds.nextToken(); String sql = cmds.nextToken("\r\n"); System.out.println("Processing: " + sql); try { // Try the query stmt = election.sql(sql); } catch (DBException de) { System.out.println("Error: " + sql + "\n" + de); } user = new ClientAddr(who.getAddress(), who.getPort()); // Send the response (will be NULL rsp if DBException) servThread.sendData(user, formatResults("QUERY_RSP " + qnum, stmt)); } } /** * A simple sleep routine * @param a the number of SECONDS to sleep */ private void sleep(int a) { try { Thread.currentThread().sleep(a * ONE_SECOND); } catch (InterruptedException e) { } } }` The run method for this thread will query the database every 30 seconds. Since this is still a demonstration applet, a simple toggle was added to cause the data to change with each read. The `formatResults()` method accepts a String and an SQLStmt class and creates a single return string consisting of the request type String, followed by the number of rows and columns in the data. This is followed by the data itself. This routine is the only method in the class that reads an SQLStmt object. Because the thread receives connection requests asynchronously, access to SQLStmt objects must be protected: `synchronized(sql) { ret += sql.numRows() + " " + sql.numCols() + " " + sql; }` When the thread queries the database, it will update the class variable: `results`. The update must also be synchronized to protect the update from corrupting a transmission. `synchronized (results) { results = nn; }` The entire method could have been protected, but good technique keeps protected ranges to the absolute minimum possible. Protecting entire methods will tie up the object for the time that the method executes. It isn't necessary to protect the database access, because the result is stored in temporary variable `nn`. The only critical piece of code is the assignment itself. In Chapter 9, the `recvNewData()` method performed no actions, but now clients can send queries to the server for evaluation. The method first checks to make sure the request is a valid `QUERY` command. If it is, the query string is extracted from the data along with the query number. The query is sent to the database, and a response is formulated for the caller. If the query fails, a normal response is sent, with the number of rows and columns set to zero. ## Election Client The SimpleClientApplet class will serve as the basis for the Election class. The server needed to implement checking only for the `QUERY` command, but as the client, the applet will have to parse and display both normal `RESULTS` and specific `QUERY_RSP` packets. In addition, logic is added to display the additional database responses. The display showing the number of users is maintained, with the new database fields being displayed under the active connections string. Listing 10.12 shows the Election applet source code. Listing 10.12. The Election applet class. `import java.applet.*; import java.awt.*; import java.net.*; import java.io.*; import java.util.*; import DGTPClient; public class Election extends Applet implements LiveDataNotify { private static final int SPACING = 70; private boolean init = false; DGTPClient ct = null; int destPort; String destHost = null; String users = null; String results[][]; int nRows = 0, nCols = 0; /** * Standard initialization method for an applet */ public void init() { if ( init == false ) { init = true; resize(500,500); String strPort = getParameter("PORT"); if ( strPort == null ) { System.out.println("ERROR: PORT parameter is missing"); strPort = "4545"; } destPort = Integer.valueOf(strPort).intValue(); destHost = getDocumentBase().getHost(); } } /** * Standard paint routine for an applet. * @param g contains the Graphics class to use for painting */ public void paint(Graphics g) { g.drawString("Active connections: " + getUsers(), 0, 100); // Paint the headings g.drawString("Candidate", SPACING * 0, 120); g.drawString("State", SPACING * 1, 120); g.drawString("Votes", SPACING * 2, 120); g.drawString("% Reporting", SPACING * 3, 120); g.drawString("Electorial Votes", SPACING * 4, 120); // Display the contents of the database for ( int x = 0; x < nRows; x++ ) { for ( int y = 0; y < nCols; y++ ) g.drawString(results[x][y], SPACING * y, 140 + (20 * x)); } } /** * Return the name of the server */ public String getDestHost() { return destHost; } /** * Return the server port number */ public int getDestPort() { return destPort; } /** * Recv a new block of data. Parse it for display. * @param newDataBlock contains the data to parse */ public synchronized void recvNewData(byte[] newDataBlock) { String cmd = new String(newDataBlock, 0); StringTokenizer cmds = new StringTokenizer(cmd, " \t"); String current = cmds.nextToken(); // Number of users update if (current.equals("CLIENTS")) users = cmds.nextToken(); // Entire new database image else if (current.equals("RESULTS")) { nRows = Integer.valueOf(cmds.nextToken()).intValue(); nCols = Integer.valueOf(cmds.nextToken()).intValue(); results = new String[nRows][nCols]; for ( int x = 0; x < nRows; x++ ) { for ( int y = 0; y < nCols; y++ ) { results[x][y] = cmds.nextToken("|\r\n"); } } } // QUERY response is unimplemented because // this applet currently sends no QUERY commands else if (current.equals("QUERY_RSP")) { } // Cause the applet to receive a paint request repaint(); } /** * Return either the users string if it has been * filled in or return the unknown string. */ public synchronized String getUsers() { if (users != null) return users; return "Unknown at this time"; } /** * Standard applet start method. Launch the * DGTP client thread. */ public void start() { ct = new DGTPClient(this); ct.start(); } /** * Standard applet stop method. Kill the * DGTPClient thread. */ public void stop() { ct.terminate(); } /** * Notification if a server connection was refused * by the host. Needed to satisfy the interface, but * otherwise unimplemented. */ public void connectRefused() { } }` The paint routine uses a `SPACING` constant to position all the data, relying on the sample table having at most four rows. Chapter 11, "Building a Live Data Applet," will dress up the applet display and expand the table to a full 100 rows! The data is stored in a two-dimensional String array within the applet. The `recvNewData()` method parses the data block into this array. Because the data is formatted into columns separated by a pipe character, a simple StringTokenizer object can completely extract the individual columns. Notice that the parsing criteria is altered after the number of rows and columns is extracted. The method `nextToken()` can accept an argument, which, if present, will become the separating characters for successive tokens. In this case, the parser changed from looking for tokens separated by spaces to looking for tokens separated by pipe characters. The routine relies on the server placing at least a single space into an otherwise null column. If this isn't done, the StringTokenizer will completely skip the column and a later `nextToken()` call will fail. Because the applet behaves in an essentially synchronous fashion, only the asynchronous method `recvNewData()` needs to be protected. This ensures that the applet will not paint while its data is being refreshed. Figure 10.2 shows the display of the Election applet. Figure 10.2 : Output of the Election applet. ## Summary This chapter covers a lot of ground-you learn how to create native methods in C and how to create and manipulate Java internal objects. You should now be familiar with method signatures, as well as the majority of standard Java helper functions. Both Java calling C and C calling into Java have been covered, and using a static initializer has been introduced for loading native libraries. In addition to native methods, the data communications concept of chaining has been introduced. The DGTP protocol is now relatively mature and can form the basis for a wide array of client/server applications. Chapter 11 will complete this section on managing live data, which has developed the internals of a sophisticated client/server applet, and address the one area not yet covered, the applet's user interface. Contact reference@developer.com with questions or comments. Copyright 1998 EarthWeb Inc., All rights reserved. PLEASE READ THE ACCEPTABLE USAGE STATEMENT. Copyright 1998 Macmillan Computer Publishing. All rights reserved. |