Java Client Library

[ Programmer Resources | Introduction | Example | Working With Data Types | Error Handling |
Performing Callbacks | Maintaining Interface Extensibility | Application Programmer's Interface ]

Programmer Resources

Programmers using the Java Client Library will find the following links useful:
  1. The Java Core Platform API (version 1.0.2)
  2. "Object-Relational Databases on the WWW: Design and Implementation", a paper describing, among other things, the design decisions made in the Java Client Library. (Available in postscript, PDF formats.)

Introduction

The Java Client Library (JCL), which is part of the Jaguar project, is a
Java package which acts as a protocol layer between a PREDATOR server and a Java-based client entity, for example, an applet or application. It manages connections to the server and provides a simple interface through which clients can process queries. In this respect, the JCL is very similar to a JDBC driver.

In addition, the JCL offers other important features necessary to support Object-Relational DBMSs like PREDATOR. Specifically, the JCL supports:

  1. Callback Mechanisms
  2. Type Extensibility
  3. Interface Extensibility

An Example

Opening a Connection

The first step in establishing a connection to a PREDATOR server through the JCL is to create a
ServerSite object. This identifies the location of the server to which the connection will be made. For example,
ServerSite S = new ServerSite("turing.csuglab.cornell.edu", 9700);
identifies a database server accepting connections on port 9700 of turing.csuglab.cornell.edu. If the client entity is an applet, it will only be able to make a connection to a database server running on the host from which it was downloaded; this is a restriction imposed by the Java security manager. In this case, the client should supply the following information to the constructor:
ServerSite S = new ServerSite(getCodeBase().getHost(), 9700);
(Note that it is possible to run a session with a server which does not run on the machine from which the applet was downloaded. This is achieved by using a JCLReflector object, which acts like a proxy for the client.) Once the database server's site has been identified, a Connection object should be created. Then, open the connection, supplying the site constructed above:
Connection C = new Connection();
try {
  C.open(S);
} catch (java.net.UnknownHostException X) {
  // The supplied host is unknown; did you spell the host's name correctly?
} catch (CannotConnectException X) {
  // The connection could not be established; is this a PREDATOR server?
} catch (ConnectionAlreadyOpenException X) {
  // The connection object is already open; close it first
}

Sending Queries

Once the connection is open, it can be used to process queries. To process a query, a
Query object must first be created. To create a query, the query text must be supplied:
try {
  Query Q = new Query("SELECT R.pic, R.name FROM Renoir R");
} catch (BadQueryException X) {
  // The query is improperly formatted
}
A query is improperly formatted if a semicolon (;) appears anywhere in the query. Semicolons are disallowed because only one query can be associated with a Query object. (Since semicolons delimit queries, a concatenation of two of more queries includes at least one semicolon.)

Then, the query can be submitted via a call to processQuery(Query, boolean):

QueryResult R;
try {
  R = C.processQuery(Q, false);
} catch (NoOpenConnectionException X) {
  // The connection must first be opened
} catch (UnterminatedQueryException X) {
  // The previous query was not terminated via a call to endQuery()
}
After the query has been processed, the query's transactional context at the server has not yet been deleted, i.e., the transaction has not yet been committed or aborted. This permits callbacks to be guaranteed the same ACID properties queries have. To end the query's transaction at the server, a call must be made to endQuery(). Until this call is made, no new queries can be processed, nor can the connection be closed. (If callback properties are unimportant, the boolean parameter can be set to true, and the query will be ended automatically, as soon as all of the results have been read completely.)

Processing Query Results

A
QueryResult is dummy interface which is extended by two classes; that is to say, calls to processQuery(Query) will return either a RecordStreamResult object, a UnformattedResult object, or a null reference. If a null reference was returned, the query submitted produced no results. (For example, this occurs when an update is submitted.) To determine the type of object returned, use Java's instanceof operator:
if (R == null) {
  // There was no result from the query
} else if (R instanceof RecordStreamResult) {
  RecordStreamResult RSR = (RecordStreamResult)R;
  // Process the record stream
} else if (R instanceof UnformattedResult) {
  UnformattedResult UR = (UnformattedResult)R;
  // Process the unformatted result
}
The two result types, RecordStreamResult and UnformattedResult, correspond to the two types of responses sent by a PREDATOR server. When the result is a relation, a RecordStreamResult is returned. When the result is textual data, an UnformattedResult is returned. For example, if the query "SELECT * FROM _STABLES" is submitted, the result is a relation. But is the query "list rels" is submitted, the result is textual.

Unformatted Results

Since unformatted results are textual, the only access method is via the function toString(). For example:
UnformattedResult UR = (UnformattedResult)R;
String Text = UR.toString();
System.out.println("Result: " + Text);

Record Stream Results

Record stream results are more complicated. A
RecordStreamResult object is an iterator interface over a stream of records. A record can be removed from the stream via a call to getNextRecord(SubclassSelector). (For simplicity's sake, a null reference should be passed as a parameter to this method. Non-null references should only be passed when interface extensibility is required.) The stream can also report that the last record has been removed via a call to isEmpty(). Records are returned in the form of arrays of DataObjects. These objects, in conjunction with the schema available via a call to getSchema(), allow the client to work with result set data. To find out more about how data types are managed in the JCL, see the Data Types section.

Working with Data Types

PREDATOR servers maintain a table of abstract data types (ADT) which determine the types of data the server can manage. Clients which interact with such a server must also maintain information about these types. The JCL manages an ADT table for this purpose. For each ADT supported by the server, a Java class is written which extends the
ADT class. These classes inform the JCL about the type; for instance, the classes give the type's name, indicate if the type's binary representation is of variable length, etc.

It is important to note that the ADT classes are not values. That is to say, the ADT_int object maintained by the JCL is not an integer. Rather, it is a container for information about integers. ADT objects also provide methods to create new DataObjects, which are value objects. For example, the JCL uses the ADT_image object it maintains to create a new ImageObject, which represents an actual image. (A JCL client is never called upon to create new objects; all objects are returned via calls to RecordStreamResult.getNextRecord(SubclassSelector). However, it is important to understand the conceptual distinction between ADT classes and the objects they manage.)

As previously mentioned, the result tuples from queries are arrays of DataObjects. These objects are only minimally functional insofar as their true properties are hidden. The DataObject class defines a single method, toString(ADTMetaInfo), which prints a textual version of the object. For instance, an DoubleObject will print its value, e.g., "3.141592654". (The parameter ADTMetaInfo is mandatory, and is explained below. If this parameter is omitted, the method invoked will be the toString() method of the Java Core Object class, giving unexpected results.)

To exploit hidden functionality of DataObjects, the Java operator instanceof must be used. Although clients may check to see if a specific type has been returned, e.g., an ImageObject, this is not recommended. To permit extensibility, common functionality that data objects offer has been grouped into interfaces, which the data objects can implement. These interfaces include:

  1. CallbackObject: The object is capable of calling back to the server for its "full value". (See callbacks for more information.)
  2. DisplayObject: The object is capable of displaying itself.
  3. MIMETypeObject: The object is a MIME type object; a browser can display it, provided a URL.
So, given a data object, client code might look like this:
DataObject tuple[] = RSR.getNextRecord(null);
for (int i = 0; i < tuple.length; i++) {
  ADTMetaInfo MetaInfo = RSR.getSchema().getAttribute(i).getMetaInfo();
  ADT TypeADT = RSR.getSchema().getAttribute(i).getTypeADT();
  if (tuple[i] instanceof DisplayObject) {
    ((DisplayObject)tuple[i]).display(MetaInfo);
  } else if ((tuple[i] instanceof MIMETypeObject) &&
             (tuple[i] instanceof CallbackObject)) {
    try {
      getAppletContext().
        showDocument(C.getCallbackObjectURL((CallbackObject)Object,
	                                    TypeADT,
	                                    MetaInfo),
		     "Requested Object");
    } catch (Exception X) {
      // Something went wrong
    }
  } else
    System.out.println(tuple[i].toString(MetaInfo));
}

Meta-Information

As you have noticed in the above examples, it is necessary to have an
ADTMetaInfo object in order to work with a DataObject. This object represents meta-information about the object, and is available from the schema of the record from which the object was obtained. For instance:
DataObject tuple[] = RSR.getNextRecord(null);
for (int i = 0; i < tuple.length; i++) {
  ADTMetaInfo MetaInfo = RSR.getSchema().getAttribute(i).getMetaInfo();
  ADT TypeADT = RSR.getSchema().getAttribute(i).getTypeADT();
  DataObject Object = tuple[i];
  /* 
   * Now MetaInfo is the meta-information object corresponding to 
   * Object, which is an object of type TypeADT.
   */
}
Meta-information is used in PREDATOR to allow ADTs to maintain domain information. For instance, an attribute of a result relation might contain an attribute of the Image type. The database server knows that objects in the column are images, but nothing more. (This is an artifact of the structure of PREDATOR; the database server knows nothing about images. All work with image objects is performed by the image ADT, which contains all the domain semantics.) Thus, if some semantic information is common to all the objects, it must be stored in every object. To prevent such a waste of space, ADTs are permitted to create meta-information for every attribute. In this object, which is stored with the attribute, the ADT can store information common to all the objects in that column of the relation. For instance, if a table contains a column of images which are known to have the same size and format, e.g., 100 KB JPEG images, this information is stored in the meta-info by the ADT. As a result, whenever one works with data objects, the meta-info of that object must also be present. (Otherwise, all the necessary information would not be available to the ADT when working with the object.)

Type Extensibility

Because the server's ADT table is extensible, the client must be prepared to work with new ADTs dynamically. For instance, if the server is augmented with another data type, e.g., video, the JCL must have a corresponding
ADT class for that type. In order to prevent the JCL from having to be rewritten every time a new type is added, the ADT classes are dynamically loaded when a connection is made to a PREDATOR server.

When the JCL opens a connection, the database server sends over a list of type names which corresponds to the types supported by the server. Using these names, the JCL queries the web server from which it was loaded for the corresponding ADT classes. So, for example, when the database server sends over the type name "audio", the JCL attempts to load the class "PREDATOR.ADT.ADT_audio". If the load is successful, the client can now work with that type. If not, result values of that type are ignored by the JCL.

Error Handling

In working with the JCL, errors crop up from time to time. These errors can come from two different sources: the database server, and the JCL itself.

Server Errors

A database server error may arise if the queries sent are inappropriate. Error handling in the PREDATOR server is asynchronous; as a result, errors can be reported at any time. To check if the server has noted an error, use the
isError() function. If an error has occurred, the actual error (which provides messages and stack information) can be obtained via a call to getError(). Applications should check for errors at the following times:
  1. After opening connections
  2. After processing queries
  3. After closing connections

JCL Errors

As is evident from the API, the JCL makes copious use of exceptions. These exceptions are designed to give accurate information as to why a JCL method failed. For instance, if a connection is closed before it is opened, a
NoOpenConnectionException is thrown. This makes designing interfaces using the JCL much easier.

In the (hopefully, rare) instance that the JCL experiences an error not caused by faulty use, a InternalError is thrown. If an internal error occurs in your application, please contact PREDATOR support.

Performing Callbacks

When query results contain large objects, such as images or audio clips, it makes sense to suspend the delivery of these objects until they are requested explicitly by the client. For instance, if a result contains hundreds of large images and a client is likely to view a small fraction of them, the images need not be delivered until the client requests them. When a client requests an object whose delivery has been suspended, the request is termed a callback. In the current implementation, all data types which are "large" require callbacks. This includes images, audio clips, rasters, polygons, etc.

If a result DataObject implements the CallbackObject interface, it is a callback object. To access the object's full value, a callback request must be made. For instance, to access the actual image in a result ImageObject, the server must be contacted.

Callback requests occur via HTTP requests. (Interestingly, these requests are made not of a web server, but directly of the PREDATOR server. The database server recognizes HTTP requests as callbacks, decodes them, and serves the required objects.) To create a connection by which the object can be accessed, a URL is necessary. The JCL provides a way to obtain a URL for a CallbackObject through the getCallbackObjectURL(CallbackObject, ADT, ADTMetaInfo) method.

Once the URL is obtained, it can be used in many ways to fetch the "full value". If the object is a MIME type object and is to be displayed, the URL can be handed to the browser, which will display the object. If the object is to be fetched into memory, a java.net.URLConnection can be created, the the stream can be read into an internal array.

Maintaining Interface Extensibility

Interface extensibility corresponds to dynamically adding functionality to a set of data types. For example, consider the traditional Geographic Information System (GIS) types: points, polygons, and raster satellite images. In a generic interface, it is perfectly acceptable for each of these types to display themselves in an independent manner. Frequently, however, a new interface requires the old data types to behave differently. For example, a GIS interface which deals with California-specific data might wish to display the points, polygons, and rasters properly situated on a map of California. How is this to be accomplished if each data type describes how it is to be displayed independently?

The JCL accomplishes interface extensibility by forcing each ADT's Java class to export to the system the minimum functionality class (MFC) for objects of that type, so that the data type will operate properly. For instance, the polygon data type specifies that all objects it deals with must at least provide the functionality of the PolygonObject class. Interfaces are free to use any subclass of this class they desire. (The MFC is exported by the method getObjectClass(ADTMetaInfo).)

To implement interface extensibility, interfaces decide on the extended functionality they require from the data types. For instance, in the above example, the interface designer might define an interface as below:

public interface CaliforniaObject {
  public void DrawYourself(CaliforniaMap C);
}
The designer then procedes to subclass the MFC of each ADT from which she desires the added functionality. Continuing the example, the designer would implement a class as below:
public class CaliforniaPolygon extends PolygonObject implements CaliforniaObject {
  public void DrawYourself(CaliforniaMap C)
  {
    /* ... */
  }
}
for each of the data types which will support the new functionality. These classes are called added functionality classes (AFC). Finally, the designer must inform the JCL object that the AFCs should be used as a substitutes for the MFCs they correspond to. This is accomplished by providing a SubclassSelector to calls to getNextRecord(SubclassSelector) that maps MFCs to their corresponding AFCs. For instance, the mapping might be, "Given class foo, if the class P(foo) exists, use it instead." The appropriate precautions to insure the mapping preserves each ADT's minimum functionality are taken, and then the AFC objects are substituted for objects of the MFC.

Application Programmer's Interface

The API for the JCL has been automatically generated using javadoc. Is it available in
HTML format. If you are interested in browsing the API to familiarize yourself with the JCL, we recommend that you begin with the PREDATOR.Connection object, which is the main interface to the JCL for any Java program.
Back to PREDATOR Home Page