All Categories :
Java
Chapter 10
Native Methods and Java
CONTENTS
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
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 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.
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.
|
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.
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.
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.
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.
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.
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.
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.
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.
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 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:
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.
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.
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.