Using the Java Object

About the Java Object

The Java object provides a mechanism that is similar to the Java Native Interface (JNI) for instantiating Java classes and accessing fields and methods on the resultant objects. You can create hybrid applications that contain both Java and DATA step code.

CLASSPATH and Java Options

In previous versions of SAS, Java classes were found using the JREOPTIONS system option.
In SAS 9.2, you must set the CLASSPATH environment variable so that the Java object can find your Java classes. The Java object represents an instance of a Java class that is found in the current Java classpath. Any class that you use must appear in the classpath. If the class is in a .jar file, then the .jar filename must appear in the classpath.
How you set the CLASSPATH environment variable depends on your operating environment. For most operating systems, you can set the CLASSPATH environment variable either locally (for use only in your SAS session) or globally. Table 23.1 shows methods and examples for different operating environments. For more information, see the SAS documentation for your operating environment.
Setting the CLASSPATH Environment Variable in Different Operating Environments
Operating Environment
Method
Example
Windows
Globally
Windows System Environment Variable in Control Panel
Control Panelthen selectSystemthen selectAdvancedthen selectEnvironment Variables (Windows XP Classic view)
SAS configuration file
set classpath c:\HelloWorld.jar
Locally
SAS command line
-set classpath c:\HelloWorld.jar
UNIX
Globally
SAS configuration file
set classpath ~/HelloWorld.jar
Locally
EXPORT command1
export classpath=~/HelloWorld.jar;
z/OS
Globally
TKMSENV data set
set TKJNI_OPT_CLASSPATH=/u/userid/java:
   /u/userid/java/test.jar: asis
Locally
Not available
VMS
Globally
Command line2
$ define java$classpath disk:[subdir] 
   abc.jar, disk:[subdir2]def.jar
detach_template.com script that is generated in sas$root:[misc.base] at installation
define java$classpath disk:[subdir] 
   abc.jar, disk:[subdir2]def.jar
Locally
Not available
1The syntax depends on the shell.
2The command line should be defined before you invoke SAS so that the process that the JVM actually runs in gets the definition as well.

Restrictions and Requirements for Using the Java Object

The following restrictions and requirements apply when using the Java object:
  • The Java object is designed to call Java methods from SAS. The Java object is not intended to extend the SAS library of functions. Calling PROC FCMP functions is much more efficient for fast in-process extensions to the DATA step, especially when large data sets are involved. Using the Java object to perform this type of processing with large data sets takes significantly more time.
  • The only Java Runtime Environments (JREs) that are supported by SAS are those that are explicitly required during the installation of the SAS software.
  • The only Java options that are supported by SAS are those that are set when SAS is installed.
  • Ensure that your Java application runs correctly before using it with the Java object.
  • The use of a percent character (%) in the first byte of text output by Java to the SAS log is reserved by SAS. If you need to output a % in the first byte of a Java text line, it must be escaped with another percent immediately next to it (%%).

Declaring and Instantiating a Java Object

You declare a Java object by using the DECLARE statement. After you declare the new Java object, use the _NEW_ operator to instantiate the object, using the Java object name as an argument tag.
declare javaobj j;
j = _new_ javaobj("somejavaclass");
In this example, the DECLARE statement tells the compiler that the object reference J is of type java. That is, the instance of the Java object is stored in the variable J. At this point, you have declared only the object reference J. It has the potential to hold a component object of type java. You should declare the Java object only once. The _NEW_ operator creates an instance of the Java object and assigns it to the object reference J. The Java class name, SOMEJAVACLASS, is passed as a constructor argument, which is the first-and-only argument that is required for the Java object constructor. All other arguments are constructor arguments to the Java class itself.
As an alternative to the two-step process of using the DECLARE statement and the _NEW_ operator to declare and instantiate a Java object, you can declare and instantiate a Java object in one step by using the DECLARE statement as a constructor method. The syntax is as follows:
DECLARE JAVAOBJobject-name(“java-class”, <argument-1, … argument–n>);
For more information, see the DECLARE Statement, Java Object in SAS Component Objects: Reference and the _NEW_ Operator, Java Object in SAS Component Objects: Reference.

Accessing Object Fields

Once you instantiate a Java object, you can access and modify its public and class fields in a DATA step through method calls on the Java object. Public fields are non-static and declared as public in the Java class. Class fields are static and accessed from Java classes.
Method calls to access object fields have one of these forms, depending on whether you are accessing non-static or static fields:
GETtypeFIELD(“field-name”, value);
GETSTATICtypeFIELD(“field-name”, value);
Method calls to modify object fields have one of these forms, depending on whether you access static or non-static fields:
SETtypeFIELD(“field-name”, value);
SETSTATICtypeFIELD(“field-name”, value);
Note: The type argument represents a Java data type. For more information about how Java data types relate to SAS data types, see Type Issues. The field-name argument specifies the type for the Java field, and value specifies the value that is returned or set by the method.
For more information and examples, see Dictionary of Java Object Language Elements in SAS Component Objects: Reference.

Accessing Object Methods

Once you instantiate a Java object, you can access its public and class methods in a DATA step through method calls on the Java object. Public methods are non-static and declared as public in the Java class. Class methods are static and accessed from Java classes.
Method calls to access Java methods have one of these forms, depending on whether you are accessing non-static or static methods:
object.CALLtypeMETHOD (“method-name”, <method-argument-1 …, method-argument-n>,
<return value>);
object.CALLSTATICtypeMETHOD (“method-name”, <method-argument-1 …, method-argument-n>,
<return value>);
Note: The type argument represents a Java data type. For more information about how Java data types relate to SAS data types, see Type Issues.
For more information and examples, see Dictionary of Java Object Language Elements in SAS Component Objects: Reference.

Type Issues

The Java type set is a superset of the SAS data types. Java has data types such as BYTE, SHORT, and CHAR in addition to the standard numeric and character values. SAS has only two data types: numeric and character.
The following table describes how Java data types are mapped to SAS data types when using the Java object method calls.
How Java Data Types Map to SAS Data Types
Java Data Type
SAS Data Type
BOOLEAN
numeric
BYTE
numeric
CHAR
numeric
DOUBLE
numeric
FLOAT
numeric
INT
numeric
LONG
numeric
SHORT
numeric
STRING
character1
1Java string data types are mapped to SAS character data types as UTF-8 strings.
Other than STRING, it is not possible to return objects from Java classes to the DATA step. However, it is possible to pass objects to Java methods. For more information, see Passing Java Object Arguments.
Some Java methods that return objects can be handled by creating wrapper classes to convert the object values. In the following example, the Java hash table returns object values. However, you can still use the hash table from the DATA step by creating simple Java wrapper classes to handle the type conversions. Then you can access the dhash and shash classes from the DATA step.
/* Java code */
import java.util.*;

public class dhash
{
   private Hashtable  table;

   public dhash()
   {
      table = new Hashtable  ();
   }

   public void put(double key, double value)
   {
      table.put(new Double(key), new Double(value));
   }

   public double get(double key)
   {
      Double ret = table.get(new Double(key));
      return ret.doubleValue();
   }
}

import java.util.*;

public class shash
{
   private Hashtable  table;

   public shash()
  {
      table = new Hashtable  ();
   }

   public void put(double key, String value)
  {
      table.put(new Double(key), value);
  }

   public String get(double key)
   {
      return table.get(new Double(key));
   }
}
/* DATA step code */
data _null_;
   dcl javaobj sh('shash');
   dcl javaobj dh('dhash');
   length d 8;
   length s $20;
  
   do i = 1 to 10;
      dh.callvoidmethod('vput', i, i * 2);
   end;

   do i = 1 to 10;
      sh.callvoidmethod('put', i, 'abc' || left(trim(i)));  end;

   do i = 1 to 10;
      dh.calldoublemethod('get', i, d);
      sh.callstringmethod('get', i, s);
      put d= s=;
   end;
run;
The following lines are written to the SAS log:
d=2 s=abc1
d=4 s=abc2
d=6 s=abc3
d=8 s=abc4
d=10 s=abc5
d=12 s=abc6
d=14 s=abc7
d=16 s=abc8
d=18 s=abc9
d=20 s=abc10

Java Objects and Arrays

You can pass DATA step arrays to Java objects.
In the following example, the arrays d and s are passed to the Java object j.
/* Java code
*/
import java.util.*;
import java.lang.*;
class jtest
{
   public void dbl(double args[])
  {
      for(int i = 0; i < args.length; i++)
         System.out.println(args[i]);
  }

  public void str(String args[])
  {
     for(int i = 0; i < args.length; i++)
        System.out.println(args[i]);
  }
}
/* DATA Step code */
data _null_;
   dcl javaobj j("jtest");
   array s{3} $20 ("abc", "def", "ghi");
   array d{10} (1:10);
   j.callVoidMethod("dbl", d);
   j.callVoidMethod("str", s);
run;
The following lines are written to the SAS log:
1.0
2.0
3.0
4.0
5.0
6.0
7.0
8.0
9.0
10.0
abc
def
ghi
Only one-dimensional array parameters are supported. However, it is possible to pass multidimensional array arguments by taking advantage of the fact that the arrays are passed in row-major order. You must handle the dimensional indexing manually in the Java code. That is, you must declare a one-dimensional array parameter and index to the subarrays accordingly.

Passing Java Object Arguments

While it is not possible to return objects from Java classes to the DATA step, it is possible to pass objects, as well as strings, to Java class methods.
For example, suppose you have the following wrapper classes for java/util/Vector and its iterator:
/* Java code */
import java.util.*;

class mVector extends Vector
{
   public mVector()
  {
     super();
  }

  public mVector(double d)
  {
     super((int)d);
  }

  public void addElement(String s)
  {
     addElement((Object)s);
  }
}

import java.util.*;
public class mIterator
{
   protected mVector m_v;
   protected Iterator iter;

   public mIterator(mVector v)
   {
      m_v = v;
      iter = v.iterator();
   }

  public boolean hasNext()
  {
      return iter.hasNext();
  }

  public String next()
  {
     String ret = null;
     ret = (String)iter.next();
     return ret;
  }
}
These wrapper classes are useful for performing type conversions (for example, the mVector constructor takes a DOUBLE argument). Overloading the constructor is necessary because java/util/Vector's constructor takes an integer value, but the DATA step has no integer type.
The following DATA step program uses these classes. The program creates and fills a vector, passes the vector to the iterator's constructor, and then lists all the values in the vector. Note that you must create the iterator after the vector is filled. The iterator keeps a copy of the vector's modification count at the time of creation, and this count must stay in synchronization with the vector's current modification count. The code would throw an exception if the iterator were created before the vector was filled.
/* DATA step code */
data _null_;
   length b 8;
   length val $200;
   dcl javaobj v("mVector");

   v.callVoidMethod("addElement", "abc");
   v.callVoidMethod("addElement", "def");
   v.callVoidMethod("addElement", "ghi");
   dcl javaobj iter("mIterator", v);

   iter.callBooleanMethod("hasNext", b);
   do while(b);
      iter.callStringMethod("next", val);
      put val=;
      iter.callBooleanMethod("hasNext", b);
   end;

   m.delete();
   v.delete();
   iter.delete();
run;
The following lines are written to the SAS log:
val=abc
val=def
val=ghi
One current limitation to passing objects is that the JNI method lookup routine does not perform a full class lookup based on a given signature. This means that you could not change the mIterator constructor to take a Vector as shown in the following code:
/* Java code */
public mIterator(Vector v)
{
    m_v = v;
    iter = v.iterator();
}
Even though mVector is a subclass of Vector, the method lookup routine cannot find the constructor. Currently, the only solution is to manage the types in Java by adding new methods or by creating wrapper classes.

Java Exceptions

Java exceptions are handled through the EXCEPTIONCHECK, EXCEPTIONCLEAR, and EXCEPTIONDESCRIBE methods.
The EXCEPTIONCHECK method is used to determine whether an exception occurred during a method call. If you call a method that can throw an exception, it is strongly recommended that you check for an exception after the call. If an exception is thrown, you should take appropriate action and then clear the exception by using the EXCEPTIONCLEAR method.
The EXCEPTIONDESCRIBE method is used to turn exception debug logging on or off. If exception debug logging is on, exception information is printed to the JVM standard output. By default, JVM standard output is redirected to the SAS log. Exception debugging is off by default.
For more information, see the EXCEPTIONCHECK Method in SAS Component Objects: Reference, EXCEPTIONCLEAR Method in SAS Component Objects: Reference, and the EXCEPTIONDESCRIBE Method in SAS Component Objects: Reference.

Java Standard Output

Output from statements in Java that are directed to standard output such as the following are sent to the SAS log by default.
System.out.println("hello");
The Java output that is directed to the SAS log is flushed when the DATA step ends. This flushing causes the Java output to appear after any output that was generated while the DATA step was running. Use the FLUSHJAVAOUTPUT method to synchronize the output so that it appears in the order of execution.

Java Object Examples

Example 1: Calling a Simple Java Method

This Java class creates a simple method that sums three numbers.
/* Java code */
class MyClass 
{
   double compute(double x, double y, double z)
      {
         return (x + y + z);
      }
}
/* DATA step code */
data _null_;
   dcl javaobj j("MyClass");

   rc = j.callDoubleMethod("compute", 1, 2, 3, r);

   put rc= r=;
run;
The following line is written to the SAS log:
rc=0 rc=6

Example 2: Creating a User Interface

In addition to providing a Java component access mechanism, you can use the Java object to create a simple Java user interface.
This Java class creates a simple user interface with several buttons. The user interface also maintains a queue of values that represent the sequence of button choices that are entered by a user.
/* Java code */
import java.awt.*;
import java.util.*;
import java.awt.event.*;

class colorsUI extends Frame
{
  private Button red;
  private Button blue;
  private Button green;
  private Button quit;
  private Vector list;
  private boolean d;
  private colorsButtonListener cbl;

  public colorsUI()
  {
    d = false;
    list = new Vector();
    cbl = new colorsButtonListener();

    setBackground(Color.lightGray);
    setSize(320,100);
    setTitle("New Frame");
    setVisible(true);
    setLayout(new FlowLayout(FlowLayout.CENTER, 10, 15));
    addWindowListener(new colorsUIListener());

    red = new Button("Red");
    red.setBackground(Color.red);
    red.addActionListener(cbl);

    blue = new Button("Blue");
    blue.setBackground(Color.blue);
    blue.addActionListener(cbl);

    green = new Button("Green");
    green.setBackground(Color.green);
    green.addActionListener(cbl);

    quit = new Button("Quit");
    quit.setBackground(Color.yellow);
    quit.addActionListener(cbl);

    this.add(red);
    this.add(blue);
    this.add(green);
    this.add(quit);

    show();
  }

  public synchronized void enqueue(Object o)
  {
    synchronized(list)
      {
        list.addElement(o);
        notify();
      }
  }

  public synchronized Object dequeue()
  {
    try
      {
        while(list.isEmpty())
          wait();

        if (d)
          return null;

        synchronized(list)
          {
            Object ret = list.elementAt(0);
            list.removeElementAt(0);
            return ret;
          }
      }
    catch(Exception e)
      {
        return null;
      }
  }

  public String getNext()
  {
    return (String)dequeue();
  }

  public boolean done()
  {
    return d;
  }

  class colorsButtonListener implements ActionListener
  {
     public void actionPerformed(ActionEvent e)
     {
        Button b;
        String l;
        b = (Button)e.getSource();
        l = b.getLabel();
        if ( l.equals("Quit") )
        {
           d = true;
           hide();
           l = "";
        }
        enqueue(l);
     }
  }

  class colorsUIListener extends WindowAdapter
  {
     public void windowClosing(WindowEvent e)
     {
        Window w;
        w = e.getWindow();
        d = true;
        enqueue("");
        w.hide();
     }
  }

  public static void main(String s[])
  {
      colorsUI cui;
      cui = new colorsUI();
  }
}
/* DATA step code */
data colors;
   length s $10;
   length done 8;
   drop done;

   if (_n_ = 1) then do;
    /* Declare and instantiate colors object (from colorsUI.class) */
      dcl javaobj j("colorsUI");
   end;

   /*
    * colorsUI.class will display a simple UI and maintain a
    * queue to hold color choices.
    */

   /* Loop until user hits quit button */
   do while (1);
      j.callBooleanMethod("done", done);
      if (done) then
         leave;
      else do;
       /* Get next color back from queue */
         j.callStringMethod("getNext", s);
         if s ne "" then
            output;
         end;
   end;
run;
proc print data=colors;
run;
quit;
In the DATA step code, the colorsUI class is instantiated and the user interface is displayed. You enter a loop that is terminated when you click Quit. This action is communicated to the DATA step through the Done variable. While looping, the DATA step retrieves the values from the Java class's queue and writes the values successively to the output data set.
User Interface Created by the Java Object
User Interface Created by the Java Object

Example 3: Creating a Custom Class Loader

You might not want to put all your Java classes in the classpath. You can write your own class loader to find the classes and load them. The following example illustrates how you can create a custom class loader.
In this example, you create a class, x, which resides in a folder or directory, y. You call the methods in this class by using the Java object with the classpath that includes the y folder.
/* Java code */
package com.sas;

public class x
{
   public void m()
   {
      System.out.println("method m in y folder");
   }

   public void m2()
   {
      System.out.println("method m2 in y folder");
   }
}
/* DATA step code */
data _null_;
   dcl javaobj j('com/sas/x');
   j.callvoidmethod('m');
   j.callvoidmethod('m2');
run;
The following lines are written to the SAS log.
method m in y folder
method m2 in y folder
Suppose you have another class, x, that is stored in a different folder, z.
/* Java code
*/
package com.sas;

public class z
{
   public void m()
   {
      System.out.println("method m in y folder");
   }

   public void m2()
   {
      System.out.println("method m2 in y folder");
   }
}
You can call methods in this class instead of the class in folder y by changing the classpath, but this requires restarting SAS. The following method allows for more dynamic control of how classes are loaded.
To create a custom class loader, first you create an interface that contains all the methods that you will call through the Java object—in this program, m and m2.
/* Java
code */
public interface apiInterface
{
   public void m();
   public void m2();
}
Then you create a class for the actual implementation.
/* Java code */
import com.sas.x;

public class apiImpl implements apiInterface
{
   private x x;

   public apiImpl()
   {
      x = new x();
   }

   public void m()
   {
      x.m();
   }

   public void m2()
   {
      x.m2();
   }
}
These methods are called by delegating to the Java object instance class. Note that the code to create the apiClassLoader custom class loader is provided later in this section.
/* Java code */
public class api
{
  /* Load classes from the z folder */
  static ClassLoader customLoader =  new apiClassLoader("C:\\z");
  static String API_IMPL = "apiImpl";
  apiInterface cp = null;

  public api()
    {
      cp = load();
    }

  public void m()
    {
      cp.m();
    }

  public void m2()
    {
      cp.m2();
    }

  private static apiInterface load()
    {
      try
        {
          Class aClass = customLoader.loadClass(API_IMPL);
          return (apiInterface) aClass.newInstance();
        }
      catch (Exception e)
       {
          e.printStackTrace();
          return null;
        }
    }
}
The following DATA step program calls these methods by delegating through the api Java object instance class. The Java object instantiates the api class, which creates a custom class loader to load classes from the z folder. The api class calls the custom loader and returns an instance of the apiImpl interface implementation class to the Java object. When methods are called through the Java object, the api class delegates them to the implementation class.
/* DATA step code */
data _null_;
   dcl javaobj j('api');
   j.callvoidmethod('m');
   j.callvoidmethod('m2');
run;
The following lines are written to the SAS log:
method m is z folder
method m2 in z folder
In the previous Java code, you could also use .jar files to augment the classpath in the ClassLoader constructor.
static ClassLoader customLoader = new apiClassLoader("C:\\z;C:\\temp\some.jar");
In this case, the Java code for the custom class loader is as follows. This code for this class loader can be added to or modified as needed.
import java.io.*;
import java.util.*;
import java.util.jar.*;
import java.util.zip.*;

public class apiClassLoader extends ClassLoader
{
  //class repository where findClass performs its search
  private List classRepository;

  public apiClassLoader(String loadPath)
    {
      super(apiClassLoader.class.getClassLoader());
      initLoader(loadPath);
    }

  public apiClassLoader(ClassLoader parent,String loadPath)
    {
      super(parent);
      initLoader(loadPath);
    }

  /**
   * This method will look for the class in the class repository. If
   * the method cannot find the class, the method will delegate to its parent 
   * class loader.
   *
   * @param className A String specifying the class to be loaded
   * @return  A Class object loaded by the apiClassLoader
   * @throws ClassNotFoundException if the method is unable to load the class
   */
  public Class loadClass(String name) throws ClassNotFoundException
    {
      // Check if the class is already loaded
      Class loadedClass = findLoadedClass(name);

      // Search for class in local repository before delegating
     if (loadedClass == null)
       {
         loadedClass = myFindClass(name);
       }

      // If class not found, delegate to parent
      if (loadedClass == null)
        {
          loadedClass = this.getClass().getClassLoader().loadClass(name);
        }
      return loadedClass;
    }

  private Class myFindClass(String className) throws ClassNotFoundException
    {
      byte[] classBytes = loadFromCustomRepository(className);
      if(classBytes != null)
        {
          return defineClass(className,classBytes,0,classBytes.length);
        }
      return null;
    }

  /**
   *  This method loads binary class file data from the classRepository.
   */
  private byte[] loadFromCustomRepository(String classFileName)
    throws ClassNotFoundException
    {
      Iterator dirs = classRepository.iterator();
      byte[] classBytes = null;
      while (dirs.hasNext())
        {
          String dir = (String) dirs.next();

          if (dir.endsWith(".jar"))
            {
              // Look for class in jar

              String jclassFileName = classFileName;

              jclassFileName = jclassFileName.replace('.', '/');
              jclassFileName += ".class";

              try
                {
                  JarFile j = new JarFile(dir);
                  for (Enumeration e = j.entries(); e.hasMoreElements() ;)
                    {
                      Object n = e.nextElement();

                      if (jclassFileName.equals(n.toString()))
                        {
                          ZipEntry zipEntry = j.getEntry(jclassFileName);
                          if (zipEntry == null)
                            {
                              return null;
                            }
                          else
                            {
                              // read file
                              InputStream is = j.getInputStream(zipEntry);
                              classBytes = new byte[is.available()];
                              is.read(classBytes);
                              break;
                            }
                        }
                    }
                }
              catch (Exception e)
                {
                  System.out.println("jar file exception");
                  return null;
                }
            }
          else
            {
              // Look for class in directory
              String fclassFileName = classFileName;

              fclassFileName = fclassFileName.replace('.', File.separatorChar);
              fclassFileName += ".class";

              try
                {
                  File file = new File(dir,fclassFileName);
                  if(file.exists()) {
                    //read file
                    InputStream is = new FileInputStream(file);
                    classBytes = new byte[is.available()];
                    is.read(classBytes);
                    break;
                  }
                }
              catch(IOException ex)
                {
                  System.out.println("IOException raised while reading class
file data");
                  ex.printStackTrace();
                  return null;
                }
            }
        }
      return classBytes;
    }

  private void initLoader(String loadPath)
    {
      /*
       * loadPath is passed in as a string of directories/jar files
       * separated by the File.pathSeparator
       */
      classRepository = new ArrayList();
      if((loadPath != null) && !(loadPath.equals("")))
        {
          StringTokenizer tokenizer =
            new StringTokenizer(loadPath,File.pathSeparator);
          while(tokenizer.hasMoreTokens())
            {
              classRepository.add(tokenizer.nextToken());
            }
        }
    }
}