| Return to previous page
|
The DATA step Java object is production in SAS 9.2. It was preproduction in SAS 9.0 and 9.1. New documentation for SAS 9.2 is highlighted with a blue background color.
One such object is the Javaobj, which provides a mechanism, similar to the Java Native Interface (JNI), for instantiating Java classes, and accessing fields and methods on the resultant objects.
declare javaobj j("someJavaClass");
This will declare and store an instance of a Javaobj object in the variable j. The Javaobj will represent an instance of the Java class someJavaClass found on the current Java CLASSPATH.
Typically, the declaration should be done only once (for one instance of j). In order to ensure this, the declaration can be done as follows:
if _N_ = 1 then do;
declare javaobj j("someJavaClass");
end;
Javaobj can also be instantiated via the _NEW_ statement:
declare javaobj j;
j = _NEW_ javaobj("someJavaClass");
declare javaobj h("java/util/Hashtable");
Thus the classname is a constructor argument to the Javaobj class. Any remaining arguments become constructor arguments to the Java class itself.
For example, to create a hashtable with an initial capacity of 100 and load factor of .8, we would create a wrapper class for java/util/Hashtable
import java.util.*;
public class mhash extends Hashtable
{
mhash(double size, double load)
{
super((int)size, (float)load);
}
}
and then instantiate it from the DATA step:
declare javaobj h("mhash", 100, .8);
The wrapper class is necessary because the DATA step's only
numeric type is equivalent to Java double.
For example, assume we had a simple Java class
import java.util.*;
import java.lang.*;
public class ttest
{
public int i;
public double d;
public String s;
}
We can create an instance of this class, and set/get fields
on the resulting object with the following DATA step program:
data _null_;
dcl javaobj j("ttest");
length val 8;
length str $20;
j.setIntField("i", 100);
j.setDoubleField("d", 3.14159);
j.setStringField("s", "abc");
j.getIntField("i", val);
put val=;
j.getDoubleField("d", val);
put val=;
j.getStringField("s", str);
put str=;
run;
Thus we have specific JNI-like methods for accessing fields
of different types. The first argument to any such field
method is the name of the field. The second argument is
the get/set value.
import java.util.*;
import java.lang.*;
public class ttest
{
public int i;
public double d;
public String s;
public int im()
{
return i;
}
public String sm()
{
return s;
}
public double dm()
{
return d;
}
}
We could access the Java values through Javaobj method calls:
data _null_;
dcl javaobj j("ttest");
length val 8;
length str $20;
j.setIntField("i", 100);
j.setDoubleField("d", 3.14159);
j.setStringField("s", "abc");
j.callIntMethod("im", val);
put val=;
j.callDoubleMethod("dm", val);
put val=;
j.callStringMethod("sm", str);
put str=;
run;
Note that the return value of the method is always specified as
the last parameter. For example, if we had a Java double
method m
public double m(double x, double y)
{
return x * y;
}
This method would be called from the DATA step as
length val1 val2 ret 8;
j.callDoubleMethod("m", val1, val2, ret);
import java.util.*;
import java.lang.*;
public class ttestc
{
public static double d;
public static double dm()
{
return d;
}
}
we have the DATA step code
data x;
dcl javaobj j("ttestc");
length d 8;
j.SetStaticDoubleField("d", 3.14159);
j.callStaticDoubleMethod("dm", d);
put d=;
run;
When using the Javaobj method calls, all Java numeric types are mapped to the DATA step numeric type. Thus byte, int, short, long, float, and double are all mapped to DATA step numeric. Java String values are mapped to DATA step character values (as UTF strings).
The Java char type is unsupported at this time.
It is also not possible to return objects (other than String) from Java classes to the DATA step (but it is possible to pass objects to Java methods, see Passing Object Arguments).
Some Java methods that return objects can be handled by creating wrapper classes to convert the object values. For instance, the Java hashtable returns object values, but we can still use it from the DATA step by creating a simple Java wrapper class to handle the type conversions:
import java.util.*;
public class mhash
{
private Hashtable table;
public mhash()
{
table = new Hashtable();
}
public void put(double key, double value)
{
table.put(new Double(key), new Double(value));
}
public void put(String key, String value)
{
table.put(key, value);
}
public double get(double key)
{
Object ret = table.get(new Double(key));
if (ret instanceof Double)
return ((Double)ret).doubleValue();
else
return Double.NaN;
}
public String get(String key)
{
return (String)table.get(key);
}
}
Then the mhash class can be accessed from the
DATA step:
data _null_;
length s $20;
/*
* Simple Java hash table test. mhash.class is a wrapper
* for java/util/Hashtable - so that we can pass Java strings
* directly instead of generic Objects.
*/
declare javaobj h("mhash");
/* Load up the table */
h.callVoidMethod("put", "key1", "data1");
h.callVoidMethod("put", "key2", "data2");
h.callVoidMethod("put", "key3", "data3");
h.callVoidMethod("put", "key4", "data4");
h.callVoidMethod("put", "key5", "data5");
/* Retrieve a value */
h.callStringMethod("get", "key3", s);
put s=;
run;
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]);
}
}
we can pass the corresponding DATA step arrays:
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;
Array get/set array field operations are planned for the Javaobj,
but have not been implemented at this time.
Currently, only one-dimensional array parameters are fully supported. However, you can pass multidimensional array arguments by taking advantage of the fact that the arrays are passed in row-major order. You have to handle the dimensional indexing manually on the Java side, that is, declare a one-dimensional array parameter and index to the subarrays accordingly.
For example, assume we had the following wrapper classes for java/util/Vector and its iterator:
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,
as in the mVector constructor which 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.
We can write a DATA step program to make use of these classes as follows:
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;
v.delete();
iter.delete();
run;
Thus we can create and fill a vector, pass it to the iterator's
constructor, and the list all the values in the vector - all
within DATA step code.
Note that the iterator must be created after the vector is filled - the iterator keeps a copy of the vector's modification count at creation, and this must stay in sync with the vector's current modification count (i.e. the above code would throw an exception if the iterator were created before the vector was filled).
One current limitation to passing objects is that the JNI method lookup routine does not do a full class lookup based on a given signature. This means you could not change the mIterator constructor to take a Vector
public mIterator(Vector v)
{
m_v = v;
iter = v.iterator();
}
Even though mVector is a subclass of
Vector, the method lookup routine will not
find the constructor. This problem may be remedied by
implementing a more robust method lookup in the Javaobj. For
now, however, the only solution is to manage the types in Java
by adding new methods or creating wrapper classes.
Java exceptions are handled through the ExceptionCheck, ExceptionClear and ExceptionDescribe methods.
ExceptionCheck is used to determine if an exception occurred during a method call.
For example, suppose we have a Java class which contains a method that throws an exception:
public class a
{
public void m() throws NullPointerException
{
throw new NullPointerException();
}
}
We can call the method m and check for the exception in DATA step code as follows:
data _null_;
length e 8;
dcl javaobj j('a');
rc = j.callVoidMethod('m');
/* Check for exception. Value is returned in variable 'e' */
rc = j.ExceptionCheck(e);
if (e) then
put 'exception';
else
put 'no exception';
run;
A more complex example involves using the Java IO classes to read an external file from DATA step code.
First, we'll need a wrapper class for DataInputStream which allows us to pass a FileInputStream to the constructor. The wrapper is necessary because the constructor actually takes an InputStream, the parent of FileInputStream, and the current method lookup is not robust enough to do the superclass lookup (see the comments in Passing Object Arguments).
public class myDataInputStream extends java.io.DataInputStream
{
myDataInputStream(java.io.FileInputStream fi)
{
super(fi);
}
}
Once we have this wrapper class, we can use it to create a DataInputStream for an external file and read it until end-of-file:
\
data _null_;
length d e 8;
dcl javaobj f("java/io/File", "some_file_containing_integers");
dcl javaobj fi("java/io/FileInputStream", f);
dcl javaobj di("myDataInputStream", fi);
do while(1);
di.callIntMethod("readInt", d);
di.ExceptionCheck(e);
if (e) then
leave;
else
put d=;
end;
run;
Here the ExceptionCheck method is used to determine when the readInt method throws an EOFException, which allows us to end the input loop.
To clear an exception, call the ExceptionClear method. Using the example class a from above, we can check for an exception and then clear it:
data _null_;
length e 8;
dcl javaobj j('a');
rc = j.callVoidMethod('m');
/* Check for exception */
rc = j.ExceptionCheck(e);
if (e) then
put 'exception';
else
put 'no exception';
/* Clear the exception */
/* Check it again */
rc = j.ExceptionClear();
rc = j.ExceptionCheck(e);
if (e) then
put 'exception';
else
put 'no exception';
run;
This will give the output
exception no exception
If you call a method which throws an exception, it is strongly recommended that you check for an exception after the call. If an exception was thrown, you should perform some appropriate action and then clear the exception.
The ExceptionDescribe method is used to turn exception debug-logging on and off. If exception debugging is on, exception information will be printed to the JVM standard output (JVM standard output is redirected to the SAS log by default).
Passing a nonzero argument to ExceptionDescribe turns exception debugging on; a zero argument turns it off. Exception debugging is off by default.
For example, you can get information about what exception is thrown in the previous example:
data _null_;
length e 8;
dcl javaobj j('a');
j.ExceptionDescribe(1);
rc = j.callVoidMethod('m');
run;
This will print the following information to standard output:
java.lang.NullPointerException
at a.m(a.java:5)
Output from statements in Java which are directed to standard output, such as
System.out.println("hello");
To redirect Java standard output to a file, you can do the following:
import java.io.*;
class redirect
{
redirect()
{
}
redirect(String out)
{
FileOutputStream f;
PrintStream p;
try
{
f = new FileOutputStream(out);
p = new PrintStream(f);
System.setOut(p);
}
catch(Exception e)
{
System.out.println(e.getMessage());
}
}
void hello()
{
System.out.println("hello");
}
}
which can be called from a Javaobj:
data x;
dcl javaobj j('redirect', 'hello_out');
j.callVoidMethod('hello');
run;
In this case, standard output from the JVM will be written to the file hello_out.
Java output directed to the SAS log will be flushed when the DATA step terminates. This will cause the Java output to appear after any output that was issued while the step was running.
For instance, the following program
data _null_;
dcl javaobj j('p');
do i = 1 to 3;
j.callVoidMethod('p');
put 'In DATA Step';
end;
run;
when used with the following Java program
import java.io.*;
public class p
{
void p()
{
System.out.println("In Java class");
}
}
will produce the following output
In DATA Step In DATA Step In DATA Step In Java class In Java class In Java class
To synchronize the output so that it appears in the order of execution, use the flushJavaOutput method:
data _null_;
dcl javaobj j('p');
do i = 1 to 3;
j.callVoidMethod('p');
j.flushJavaOutput();
put 'In DATA Step';
end;
run;
This will produce the following output:
In Java class In DATA Step In Java class In DATA Step In Java class In DATA Step
For instance, we can create a simple Java user interface using the following Java classes:
import java.awt.*;
public class fr extends Frame
{
public fr()
{
super.setBackground(Color.lightGray);
super.resize(500, 400);
super.setTitle("New Frame");
super.setVisible(true);
super.show();
}
public boolean handleEvent(Event e)
{
if (e.id == Event.WINDOW_DESTROY)
{
this.hide();
return true;
}
return super.handleEvent(e);
}
}
import java.awt.*;
import java.util.*;
import java.awt.event.*;
public class colors extends fr
{
private Button red;
private Button blue;
private Button green;
private Button quit;
private Vector list;
boolean d;
whandler wh;
bhandler bh;
public colors()
{
super();
wh = new whandler(this);
bh = new bhandler(this);
addWindowListener(wh);
d = false;
list = new Vector();
GridBagLayout l = new GridBagLayout();
GridBagConstraints c = new GridBagConstraints();
this.setLayout(l);
red = new Button("Red");
red.setBackground(Color.red);
red.addActionListener(bh);
blue = new Button("Blue");
blue.setBackground(Color.blue);
blue.addActionListener(bh);
green = new Button("Green");
green.setBackground(Color.green);
green.addActionListener(bh);
quit = new Button("Quit");
quit.setBackground(Color.yellow);
quit.addActionListener(bh);
c.gridx = 0;
c.gridy = 0;
c.gridwidth = 3;
c.gridheight = 3;
l.setConstraints(red, c);
this.add(red);
c.gridx = 3;
c.gridwidth = 3;
c.gridheight = 3;
l.setConstraints(blue, c);
this.add(blue);
c.gridx = 6;
c.gridwidth = 3;
c.gridheight = 3;
l.setConstraints(green, c);
this.add(green);
c.gridx = 20;
c.gridy = 20;
c.gridwidth = 3;
c.gridheight = 3;
l.setConstraints(quit, c);
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;
}
public static void main(String[] args)
{
colors c = new colors();
}
class bhandler implements ActionListener
{
colors colors;
bhandler(colors colors)
{
this.colors = colors;
}
public void actionPerformed(ActionEvent e)
{
String c = e.getActionCommand();
if (c.equals("Quit"))
{
synchronized(this)
{
enqueue("");
}
d = true;
colors.hide();
colors.dispose();
}
else
{
synchronized(this)
{
enqueue(c);
}
}
}
}
class whandler extends WindowAdapter
{
colors c;
public whandler(colors c)
{
this.c = c;
}
public void windowClosing(WindowEvent e)
{
super.windowClosing(e);
synchronized(this)
{
enqueue("");
}
c.d = true;
c.hide();
c.dispose();
}
}
}
This Java class will create a simple UI with several buttons. It also maintains a queue of values representing the sequence of button choices entered by a user.
The 'driver' DATA step is:
data colors;
length s $10;
length done 8;
drop done;
if (_n_ = 1) then do;
/* Declare and instantiate colors object (from colors.class) */
dcl javaobj j("colors");
end;
/*
* colors.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;
Here the colors class is instantiated (and the UI is displayed). Then we enter a loop which is terminated when the Quit button is pressed (this action is communicated to the DATA step via the done variable). While looping, the DATA step retrieves values from the Java class's queue, and writes them successively to the output data set.
For example,
Obs s
1 Red
2 Blue
3 Green
4 Blue
5 Red
6 Red
7 Blue
8 Green
9 Red
Users can override the normal classpath search given in the jreoptions by creating a custom class loader. The following method is based on Zhiyong Li's SAS Application Class Loader.
Suppose we have a class x:
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");
}
}
which resides in a folder (or directory) called y. We can call methods in this class using the Javaobj with a jreoption classpath which includes the y folder.
data _null_;
dcl javaobj j('com/sas/x');
j.callvoidmethod('m');
j.callvoidmethod('m2');
run;
This will print the following output.
method m in y folder method m2 in y folder
Suppose further that we have another class x stored in a different folder z.
package com.sas;
public class x
{
public void m()
{
System.out.println("method m in z folder");
}
public void m2()
{
System.out.println("method m2 in z folder");
}
}
We can call methods in this class instead of the one in y by changing the classpath, but that may not always be a reasonable option in general. For instance, the given class could be part of the SAS installation, in which case changing the classpath could affect other applications. To further complicate things, the given class could reside in a jar file and depend on multiple other classes in other jar files.
To control class loading in cases like this, we can create a custom class loader.
To do this, we first create an interface which contains all the methods we will be called through the Javaobj, in this case methods m and m2:
public interface apiInterface
{
public void m();
public void m2();
}
Then a class for the actual implementation:
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 will be called by delegating through the Javaobj instance class:
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;
}
}
}
which is run from SAS as
data _null_;
dcl javaobj j('api');
j.callvoidmethod('m');
j.callvoidmethod('m2');
run;
The Javaobj instantiates the api class, which will create a custom class loader to load classes from the z folder. The api class calls the custom loader and returns an instance of the interface implementation class (apiImpl) to the Javaobj. Then when methods are called through the Javaobj, the api class delegates them to the implementation class.
This example will print the following:
method m in z folder method m2 in z folder
Note: You can also use jar files to augment the classpath in the classloader constructor:
static ClassLoader customLoader = new apiClassLoader("C:\\z;C:\\temp\some.jar");
Note: The Java code for the custom class loader follows. This can be extended 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
* it cannot find it, it 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());
}
}
}
}
If you are using SAS 9.2, you must set the CLASSPATH environment variable so the Javaobj can find your classes. Any class you are searching for must appear on the classpath. If the class is in a jar file, then the jar file name must appear on the classpath.
In versions previous to 9.2, classes were found using the jreoptions option. In version SP4, use
sdssas -jreoptions (-Dsas.app.class.path=your_java_classpath_here)
If you're running a version of SAS prior to SP4, use
sdssas -jreoptions (-Djava.class.path=your_java_classpath_here)
An exception to the above rule for jreoptions occurs on the Windows platforms where the change occurred at SP3.
where the exceptionCheck takes a double argument which returns the exception status, the exceptionDescribe method takes a double argument which sets the debug status, and the exceptionClear method takes no parameters.
where flushJavaOutput requires no parameters.