Using DATA Step Component Objects |
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 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.
Operating Environment | Method | Example |
---|---|---|
Windows | ||
Globally | Windows System Environment Variable in Control Panel | Control Panel System Advanced Environment 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 gets generated in sas$root:[misc.base] at installation |
define java$classpath disk:[subdir] abc.jar, disk:[subdir2]def.jar |
Locally | Not available |
|
1
The syntax depends on the
shell.
2 The command line should be defined before you invoke SAS so 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 kind 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.
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 JAVAOBJ object-name("java-class", <argument-1, ... argument-n>); |
For more information, see the DECLARE Statement and the _NEW_ Operator for the Java object in SAS Language Reference: Dictionary.
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 Java Object Language Elements in SAS Language Reference: Dictionary.
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 Java Object Language Elements in SAS Language Reference: Dictionary.
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.
Table 23.2 describes how Java data types are mapped to SAS data types when using the Java object method calls.
Java Data Type | SAS Data Type |
---|---|
BOOLEAN | numeric |
BYTE | numeric |
CHAR | numeric |
DOUBLE | numeric |
FLOAT | numeric |
INT | numeric |
LONG | numeric |
SHORT | numeric |
STRING | character1 |
1 Java 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, but 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 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 will not find the constructor. Currently, the only solution is to manage the types in Java by adding new methods or creating wrapper classes.
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, the EXCEPTIONCLEAR method, and the EXCEPTIONDESCRIBE method in SAS Language Reference: Dictionary.
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 |
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 lines are written to the SAS log:
rc=0 rc=6
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
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 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()); } } } }
Copyright © 2010 by SAS Institute Inc., Cary, NC, USA. All rights reserved.