Java Class

Java is designed to be a simple, object-oriented language for deploying secure, multithreaded network applications (both clients and servers). Prolog is ideal for building the intelligent components, expert systems and logic-bases. In combination, Java and Prolog are an ideal pair for delivering useful intelligent applications on the Internet. The Java Class encapsulates the Amzi! Logic Server for use by Java applications and applets. It includes:

In addition, you can extend the Java Class to allow Prolog to call methods in your Java code (although this is a bit tricky and is currently poorly documented by Sun).

Where to Learn About Java

If you want to learn more about Java and Java Applets, see java.sun.com/doc. You can also download Sun's Java Development Kit (JDK) from there. Netscape is developing its own interface for native methods called the Java Runtime Interface (JRI); also check out its new LiveWire and LiveConnect technology for integrating Java on both the client (Navigator) and server sides. For more information on it and Java Script see developer.netscape.com. Finally Microsoft is shipping Java support for their Internet Explorer at www.microsoft.com/ie/java.

Installing the Java Class

The Java Class and its samples require Sun's Java Development Kit (JDK) 1.0.2.

To use the Java Class, you must have Amzijava.dll somewhere in your PATH (it is provided in the \bin subdirectory which should already be in your PATH). Also you must copy LogicServer.class and LSException.class to a directory tree named amzi\ls where the amzi directory is somewhere in your CLASSPATH. A good place for this is \java\lib.

Hello Prolog from Java

After completing the installing described above you are ready to use the Java Class. To build the Hello program from Java, first open a 'DOS' window and change to the directory containing Hello.java. To compile it, type:

javac Hello.java

This will produce Hello.class which is simply run by typing:

java Hello

Java Class Overview

The Java Class uses a feature of Java called 'native methods'. This allows methods in a class to be implemented in C or C++ in a DLL. To build a Java class with native methods (that call the existing C/C++ interface the Amzi! Logic Server DLL), we needed to build a new library that conforms to the calling conventions expected by the Java runtime. The links from a Java program to the basic Amzi! library are shown in Figure 1.

Java Program   ->   Amzi! Java Class   ->   Amzijava library   ->   Amzi32 library

Figure 1

Amzi32 is the Amzi! Logic Server. Amzijava is the new library that connects the Amzi! Java Class to the Amzi! Logic Server.

In addition to this underlying architecture, there are a number of features of the Java class that were dictated by the distinct characteristics of Java. These are detailed in the following sections.

Object Oriented

The Amzi! Logic Server has already been packaged for two object-oriented development languages, C++ and Delphi. These classes allow developers to derive their own application-specific classes that encapsulate Prolog services (see "Objects and Logic-C++ Meets Prolog", PC AI May/Jun 95)

The Java implementation borrows from the ideas in these classes. It includes a main 'LogicServer' class that encapsulates a Prolog engine and its API, and an 'LSException' class used for error handling. They are both included in a Java package, 'amzi.ls'. Figure 2 is an expanded architecture diagram that illustrates the package and its classes.

Java Program   ->   Amzi! Java Class   ->   Amzijava library   ->   Amzi32 library
import amzi.ls.*;   amzi.ls.LogicServer
                    amzi.ls.LSException

Figure 2

Pointerless Methods

The biggest challenge in the design of the Amzi! Java class was the requirement for pointerless methods. The creators of Java wanted to keep their language simple, so they did not include support for pointers nor for passing parameters by reference. This meant all of the Java methods had to be implemented so they return a single value.

A fundamental data type for the Prolog interface is a Prolog term. Internally, a Prolog term is a pointer, but, since that pointer is not manipulated by the application, it can be stored as an integer, similar to how it is stored in the Visual Basic interface. Unlike VB, Java integers are 64-bits, which means they can be used for both 32- and 64-bit implementations of Amzi! Prolog.

Many Logic Server API (LSAPI) functions return error codes, and use argument pointers to pass values back and forth. The Java class, like other Amzi! wrappers, is designed to use exception handling for errors, with return values passed directly as return values, without using pointers.

There were a few LSAPI functions that required special treatment.

Issuing Prolog Queries

The LSAPI functions that issue Prolog queries return TRUE or FALSE, corresponding to Prolog success or failure, and return the unified query term as an argument. For example a function issuing the query 'available(com, Port)' will return true or false plus the term representing the query with the Port variable unified with the result.

This basic pattern is modified in the Java class, so that query methods return the Prolog term if the query was successful, and return 0 if the query fails.

Retrieving Values from Prolog Lists

The methods to retrieve values from a Prolog list also returned two items, the head of the list and a term representing the tail of the list. In this case the LSAPI was extended for Java to include two new methods, one for each of these values. One is called getHead and the other getTail.

String Conversion

Strings in Java are not C strings. Instead they are represented in unicode to accommodate the wide range of non-ASCII characters. The LogicServer methods automatically convert strings between the two formats.

Exceptions

Instead of returning error codes, all the LogicServer methods use Java's exception mechanism. The LSException class is thrown when an error occurs. The actual error message can be retrieved from an error catcher by calling the GetExceptMsg method for the LogicServer object that generated the error.

Multi Threaded

Java allows you to start multiple threads in the same program. Internally each engine is represented by an engine id, but the Java class hides this as a private class variable. So each instance of the Java Logic Server class will contain its own Prolog runtime environment.

Using the Java Class

To use the LogicServer class you import the amzi.ls package into your Java program:

import amzi.ls.*;

From there you can either instantiate a new LogicServer object and invoke its methods, or you can define a new class that extends the LogicServer class adding new methods and variables.

Java LogicServer and LSException Methods

The LogicServer class includes all the methods that give the developer full control over the Prolog engine. These include methods to:

The LSException class has no methods and is simply used to signal and catch errors as described in the section on 'Exceptions.'

Java Methods to Set Up the Logic Server

These methods provide the basic API services. They are used to initialize and close down the Prolog environment. The function, Main, runs the main/0 predicate of a loaded Prolog module.

        public native void Init(String ININame) throws LSException;
        public native void Load(String XPLName) throws LSException;
        public native boolean Main() throws LSException;
        public native void Reset() throws LSException;
        public native void Close() throws LSException;

Java Methods for Calling Prolog

These are the methods that actually call predicate in a Prolog module. The query term can be represented as a string or a Prolog term. The methods return the term representing the result, or the value 0 if the query fails.

        public native long Exec(long Term) throws LSException;
        public native long ExecStr(String Query) throws LSException;
        public native long Call(long Term) throws LSException;
        public native long CallStr(String Query) throws LSException;
        public native boolean Redo() throws LSException;
        public native void ClearCall() throws LSException;

Java Methods for Manipulating the Dynamic Database

These methods make it easy to assert and retract terms to and from Prolog's dynamic database.

        public native void Asserta(long Term) throws LSException;
        public native void Assertz(long Term) throws LSException;
        public native void Retract(long Term) throws LSException;
        public native void AssertaStr(String TermStr) throws LSException;
        public native void AssertzStr(String TermStr) throws LSException;
        public native void RetractStr(String TermStr) throws LSException;

Java Methods for String/Term Conversion

These methods convert strings to terms and terms to strings. The 'Q' version create quoted strings when necessary for atoms and strings that require delimiter symbols. They are necessary for those cases when you want to use the resulting string in another query.

TermToStr is especially useful during development to display the results of a Prolog query.

        public native String TermToStr(long Term, int Len) throws LSException;
        public native String TermToStrQ(long Term, int Len) throws LSException;
        public native long StrToTerm(String TermStr) throws LSException;
        public native int StrTermLen(long Term) throws LSException;

Java Methods for Making/Getting Prolog Types

These methods map Prolog types to Java types.

        public native long MakeAtom(String AtomStr) throws LSException;
        public native long MakeStr(String Str) throws LSException;
        public native long MakeInt(int Num) throws LSException;
        public native long MakeFloat(float Num) throws LSException;
        public native int GetTermType(long Term) throws LSException;
        public native String GetStrTerm(long Term) throws LSException;
        public native int GetIntTerm(long Term) throws LSException;
        public native int GetLongTerm(long Term) throws LSException;
        public native float GetFloatTerm(long Term) throws LSException;

Java Methods for Manipulating Structures

These methods let you create and take apart terms that represent structures. This is especially useful for retrieving arguments in a query. For example, for the query 'sibling(mary, X)' GetStrArg can be used to retrieve the second argument of the strucutre.

        public native String GetFunctor(long Term) throws LSException;
        public native short GetArity(long Term) throws LSException;
        public native long MakeFA(String Functor, int Arity) throws LSException;
        public native long GetArg(long Term, int Num) throws LSException;
        public native String GetStrArg(long Term, int Num) throws LSException;
        public native int GetIntArg(long Term, int Num) throws LSException;
        public native int GetLongArg(long Term, int Num) throws LSException;
        public native float GetFloatArg(long Term, int Num) throws LSException;
        public native long UnifyStrArg(long Term, int Num, String Str) throws LSException;
        public native long UnifyIntArg(long Term, int Num, int Val) throws LSException;
        public native long UnifyLongArg(long Term, int Num, int Val) throws LSException;
        public native long UnifyFloatArg(long Term, int Num, float Val) throws LSException;
        public native short GetArgType(long Term, int Num) throws LSException;
        public native int StrArgLen(long Term, int Num) throws LSException;

Java Methods for Manipulating Lists

These methods let you create Prolog lists, add items to lists and retrieve items from lists. The Get__Head family of functions can be used in loops to get all the items in a list (inconjunction with GetTail).

        public native long MakeList() throws LSException;
        public native long PushList(long ListTerm, long Term) throws LSException;
        public native long GetHead(long ListTerm) throws LSException;
        public native String GetStrHead(long ListTerm) throws LSException;
        public native short GetIntHead(long ListTerm) throws LSException;
        public native int GetLongHead(long ListTerm) throws LSException;
        public native float GetFloatHead(long ListTerm) throws LSException;
        public native long GetTail(long ListTerm) throws LSException;

Miscellaneous Java Methods

This method lets you get the current version number.

        public native String GetVersion() throws LSException;

Java Methods for Error Handling

This method lets you get the message for the exception that was thrown.

        public native String GetExceptMsg() throws LSException;

Java Class Shipping Advisor Sample

To use the LogicServer class you import the amzi.ls package into your Java program. From there you can either instantiate a new LogicServer object and invoke its methods, or you can define a new class that extends the LogicServer class adding new methods and variables. This example uses the first approach to implement an application that dispenses advice on how to ship a package.

The shipping advisor (Ship.java) asks the user to specify the type of package, its weight, UPS zone and a couple of shipping options. The advisor returns a list of shipping options along with their cost and transit times.

The User Interface

For simplicity, the shipping information is gathered using Java's AWT (Alternative Window Toolkit) package. Figure 3 shows the screen.

Figure 3: The shipping advisor screen

The Java code for building the screen is cumbersome and dull. Future Java development environments will no doubt automate this process in a manner similar to Delphi, Visual Basic or C++. The interesting code is how Java interacts with the LogicServer class after the user presses 'Go'.

Calling Prolog

First, a new instance of the Amzi! LogicServer is created and initialized and the compiled Prolog code file (ship.xpl) is loaded:

LogicServer ls = new LogicServer();
ls.Init("");
ls.Load("ship.xpl");

Next, all the user inputs are asserted into Prolog's dynamic database as facts of the form known(Attribute, Value). An alternative to asserting them would be to build a Prolog list and pass it during the call to Prolog. Either way Java's excellent string handling makes building the Prolog terms easy. In this case the strings are built using Java functions that retrieve text values from the user input form (see top of Figure 3).

ls.AssertaStr( "known(weight, " + weight.getText() + ")" );
if (cod.getState())
      ls.AssertaStr( "known(cod, yes)" );
else
      ls.AssertaStr( "known(cod, no)" );
ls.AssertaStr( "known(declared_value, " + decvalue.getText() + ")" );
ls.AssertaStr( "known(ups_zone, " + zone.getText()+")");
ls.AssertaStr( "known(type, " + pkgtype.getSelectedItem() + ")" );
ls.AssertaStr( "known(destination, 'USA')" );

Next the Prolog expert is consulted to provide us with a list of options. This is accomplished first by calling the option/4 predicate in the shipping advisor. It fires Prolog rules that result in a recommendation being returned in its four arguments, which represent the shipper, service, cost and delivery time. On backtracking, it returns other possible recommendations.

The query term is entered in the LSAPI function CallStr() just as it would be from a Prolog listener. As mentioned above, CallStr() returns the unified term, from which the arguments are extracted using the LSAPI GetStrArg() methods. The arguments are all extracted as text, which are added to the Java text area, 'advice'.

term = ls.CallStr("option(Shipper, Service, Cost, Delivery)");
      do
      {
         advice.appendText(ls.GetStrArg(term, 1));
         advice.appendText(" : ");
         advice.appendText(ls.GetStrArg(term, 2));
             advice.appendText("\n  $");
         amt = ls.GetIntArg(term, 3);
         famt = amt;
         advice.appendText(Float.toString(famt/100));
         advice.appendText(",  ");
         advice.appendText(ls.GetStrArg(term, 4));
         advice.appendText("\n\n");
      } while (ls.Redo());

The call to Redo() is used to cause the Prolog engine to backtrack and reunify the query term with another recommendation. This continues until there are no more recommendations, and Redo() returns FALSE. At this point the text box is filled with all the available recommendations.

Finally the Prolog engine is closed:

ls.Close();

Extending Prolog with Java Methods

The Amzi! Logic Server provides tools that let you implement your own extended predicates. These allow you to write Prolog code that directly accesses anything you like. Extended predicates are simple to implement in languages that support function pointers, such as C/C++ and Delphi.

Java does not support function pointers but Java does provide a way for native 'C' methods to invoke Java methods. Extended Prolog predicates are implemented using this indirection.

As of this writing, the documentation on how to call these Java methods from 'C' is sketchy, so for the sake of simplicity, the example uses an extended predicate that calls displayStatus to simply print a string.

Here's the Java code for displayStatus (its in our Ship class):

public static void displayStatus(String Msg)
   {
      System.out.println(Msg);
   }

Here's how the extended predicate is called from Prolog with its parameter string.

   displayStatus($Analyzing shipping options…$)

And here's the extended predicate in 'C':

TF EXPFUNC p_displayStatus(ENGid eid)
{
        struct ClassClass *myClass;
        struct Hjava_lang_String *jStr;
        int     jLen, rc;
        long    lval;
        char    *cStr;

        /* Get the string from the first parameter */
        jLen = lsStrParmLen(eid, 1);
        cStr = malloc(jLen + 1);
        if (cStr == NULL) return(FALSE);
        rc = lsGetParm(eid, 1, cSTR, cStr);

        /* Turn it into a Java string (unicode) */
        jStr = makeJavaString(cStr, jLen);
        free(cStr);

        /* Get the pointer to the class and call the method */
        myClass = FindClass(0, "Ship", TRUE);
        lval = execute_java_static_method(0, myClass, "displayStatus", 
          "(Ljava/lang/String;)V", jStr);

        return(TRUE);
}

The parameter string is retrieved from Prolog and converted into a Java string (unicode). Then, the method is actually called by execute_java_static_method().

To rebuild amzijava.dll with the new extended predicate, do the following:

  1. javac LogicServer.java
  2. javac LSException.java
  3. Copy LogicServer.class and LSException.class to am amzi subdirectory of a directory included in your CLASSPATH, e.g. \java\lib\amzi
  4. javah amzi.LogicServer
  5. javah -stubs amzi.LogicServer

Then compile amzijava.c along with LogicServer.c and linked into a multithreaded DLL called amzijava.dll. (Don't forget to save a copy of the original amzijava.dll provided by Amzi!)

This example is a simple illustration of using an extended predicate to allow Prolog code to display information to a user interface implemented in Java. This same idea can be used to allow more complex graphical output from Prolog.

Extended predicates can also be used by Prolog to retrieve information from Java and to manipulate Java classes and methods. In other words, Prolog can be given direct access to any network or graphical services available to Java.

Alternative Architectures

The above example was built as a standalone Java application for simplicity. There are a number of ways to use Java on both the client and server sides of an application. Here are a couple of alternative architectures worth noting.

Java Client, Prolog Server

The AWT user interface could be written as a Java Applet that communicates with a Prolog server by means of sockets. This is not allowed by many browsers for security reasons.

Java Client, Java+Prolog Server

Both the user interface and client-server communication could be written in Java, with the Logic Server embedded on the server side.

HTML Client, Java+Prolog Server

As the user interface is very straightforward, it could be handled entirely with HTML forms and a CGI program written in Java that accesses the Logic Server. Instead of CGI a direct connection could be made using Microsoft's ISAPI, or Netscape's NSAPI or LiveWire.

JavaScript + HTML Client, Java+Prolog Server

The user interface could be enhanced with JavaScript on the client-side to do some error checking such as ensuring a proper U.P.S. zone or weight specification.