Jawin, An Open Source Interoperability Solution
by Stuart Halloway11/14/2001
The Java/Win32 integration project (Jawin) is a free, open source architecture for interoperation between Java and components exposed through Microsoft's Component Object Model (COM) or through Win32 Dynamic Link Libraries (DLLs). The current version of Jawin is 1.0.4, build 200111031306. You can download current source and binaries here.
You can use Jawin to interact with scriptable applications such as the Microsoft Office suite. You can use Jawin to call scriptable logic components such as Microsoft's COM-based XML parsers and tools. You can also use Jawin to access Win32 API features such as the Windows registry, security APIs, and the event log. In fact, Jawin allows your Java applications to call any legacy COM- and DLL-based code, without having to write any Java Native Interface (JNI) code. Using Jawin, you can call any component that can be scripted in the Windows environment. You can also call arbitrary COM components or DLL entry points.
Jawin includes a code generator for scriptable COM components. The code generator reads a type library and automatically emits the Java stubs needed to call the component. Jawin does not include a code generator for non-scriptable COM components or DLL entry points (yet).
This document should provide everything you need to get started using Jawin. If you have any questions, please subscribe and post them to the Jawin mailing list.
Contents
- Why Jawin? (JNI Is Not Enough)
- Installing Jawin
- Calling a Scriptable COM component
- Calling a Scriptable COM component using generated stubs
- Using JawinGen to generate stubs
- Calling a DLL entry point
- Jawin Architecture
- Troubleshooting Jawin
- Resources
Why Jawin? (JNI is Not Enough)
The Java Native Interface (JNI) is a standard way to access native code from the Java platform. JNI provides a set of low-level primitives that can be used to marshal arguments from Java into native code, and vice versa.
Note the choice of the word "primitive." JNI is a very low-level approach to calling native code. Examine the code fragment below, which shows using JNI to access the registry. Only the code in bold is relevant. All of the rest of the code is devoted to managing parameters and resources, and is totally irrelevant to the task at hand.
JNIEXPORT jint JNICALL Java_NTRegistry_getDword
(JNIEnv *pEnv, jclass cls, jint hive, jstring key, jstring name)
{
unsigned long dw = -1;
unsigned long size;
HKEY hkey = NULL; jchar* psName = NULL;
jchar* psKey = NULL;
if (!getString(pEnv, key, &psKey)) goto fail;
if (!getString(pEnv, name, &psName)) goto fail;
if (ERROR_SUCCESS != ::RegOpenKeyW((HKEY)hive, psKey, &hkey)) goto fail;
size = sizeof(dw);
if (ERROR_SUCCESS == ::RegQueryValueExW(hkey, psName, NULL, NULL,
(BYTE*) &dw, &size)) goto cleanup;
fail:
cls = pEnv->FindClass("java/lang/IllegalArgumentException");
if (cls) pEnv->ThrowNew(cls, "Could not read registry key");
cleanup:
if (hkey) ::RegCloseKey(hkey);
if (psKey) free (psKey);
if (psName) free (psName);
return dw;
}
This is not the intended use for JNI. JNI is best when it is used to build a higher-level marshalling layer that provides a transparent, or near transparent, mapping between Java objects and objects on a particular native platform. In order to access the very large number of COM components and Win32 DLLs, several companies and open source projects have leveraged JNI to create more developer-friendly, Win32-specific solutions.
Jawin is one such project. I built Jawin because I was dissatisfied with JNI as a mechanism for
interop. JNI requires that you write custom native code each time you want
to add a new capability. With Jawin, all the native code is in Jawin.dll,
and no additional native code needs to be written.
Installing Jawin
To install Jawin, download and unzip the file http://staff.develop.com/halloway/code/jawin.zip to a directory of your choice. After you have unzipped the Jawin code, you should test to make sure that Jawin is working correctly. You can do this by using Jawin's Ant build script.
Jawin is built using Ant, an open
source Java build tool. In addition to providing support for compiling Java
code, Ant also makes it easy to launch most java command-line tools, including java
itself. Jawin's Ant build manifest is the file build.xml in the
root directory. If you are an experienced Ant user, you can learn a lot about
Jawin just by studying this file. To verify that Jawin is working correctly, install
Ant and execute the following Ant task:
ant -emacs "run tests"
You should see a spate of "Test succeeded" methods and no exception traces. If you do see an exception trace, Jawin is not functioning correctly in your machine. Please post your hardware and software configuration, plus the full stack trace, to the Jawin mailing list.
Calling a Scriptable COM component
Probably the most common use of Jawin is to call a scriptable COM component. The code fragment below demonstrates using Jawin to instantiate PowerPoint and build a very simple slide show.
try {
Ole32.CoInitialize();
DispatchPtr app = new DispatchPtr("new:PowerPoint.Application");
app.put("Visible", true);
DispatchPtr preses = app.getObject("Presentations");
DispatchPtr pres = (DispatchPtr) preses.invoke("add", -1);
DispatchPtr slides = pres.getObject("Slides");
DispatchPtr slide = (DispatchPtr) slides.invoke("Add", 1, 2);
DispatchPtr shapes = slide.getObject("Shapes");
DispatchPtr shape = (DispatchPtr) shapes.invoke("Item", 1);
DispatchPtr frame = shape.getObject("TextFrame");
DispatchPtr range = frame.getObject("TextRange");
range.put("Text", "Using Jawin to call COM objects");
Ole32.CoUninitialize();
}
catch (Exception e) {
e.printStackTrace();
}
You can execute this demo with the command line
ant -emacs "ppt demo"
The calls to Ole32.CoInitialize and Ole32.CoUninitialize set up and tear down the COM libraries on the current thread. You must make these calls on any thread that will be calling COM objects. The first DispatchPtr is created directly, using the new moniker followed by the ProgId for the COM class you want to use. (The name DispatchPtr comes from the fact that scriptable COM components implement the IDispatch interface.) Subsequent DispatchPtr instances are created by calling methods and
accessors, working down the object hierarchy to finally place some text on a
slide.
This code has one noteworthy advantage: it is simple. Code written in this
style requires no compile-time knowledge of the COM component you are calling. You could attempt to invoke, put, or get any name you want -- the Java compiler will not care.
This simplicity can also be seen as a disadvantage. Because it uses no type
information to describe PowerPoint, the compiler will not tell you if this code
represents a valid set of method calls and property accesses. Instead of always
using DispatchPtr, it would be nice to have actual Java classes
named Application, Presentation, Slide,
etc., with specific methods that can be verified by the Java compiler.
Jawin's code generation feature can create these helper classes, which are called stubs.
Calling a Scriptable COM Component Using Generated Stubs
The following code fragment is identical in effect to the code sample above, except it uses generated java classes as stubs for the dispatch interfaces in the type library.
try {
Ole32.CoInitialize();
_Application app = new _Application("new:PowerPoint.Application");
app.put("Visible", true);
Presentations preses = app.getPresentations();
_Presentation pres = preses.Add(-1);
Slides slides = pres.getSlides();
_Slide slide = slides.Add(1,2);
Shapes shapes = slide.getShapes();
Shape shape = shapes.Item(new Integer(1));
TextFrame frame = shape.getTextFrame();
TextRange range = frame.getTextRange();
range.setText("Jawin to call COM objects");
Ole32.CoUninitialize();
}
catch (Exception e) {
e.printStackTrace();
}
You can run this example using the Ant command line
ant -emacs "ppt stubs demo"
When using the generated stubs, you must still call Ole32.CoInitialize and Ole32.CoUninitialize in order to bootstrap the COM libraries. However, this code has the notable advantage of being strongly typed. In addition to strongly-typed references, the methods exposed by the stubs contain
strongly-typed parameter lists. Using the generated stubs, you get compile-time protection against type misuse.
Jawin also provides a mechanism for generating a second type of file: interfaces which represent any COM enums found in the type library. These interfaces scope a list of named constants inside a namespace which matches the enum name from the library. This allows the Java developers the same ability to avoid "magic number" programming that is afforded to COM programmers using VB or C++.
Using JawinGen to Generate Stubs
The stub generator used by Jawin, jawingen/jawinGenUI.exe, has both a windowed and a command-line interface. Creating the stubs is a simple process. You need only provide four pieces of information: the full path to the type library you wish to parse, a destination folder to place the output, the full package name you wish the stubs to be a part of, and the project name you wish to use to identify the output. You can provide this information through the Windows interface, or via flags on the command line. If you provide a subset of these items via the command line, the windowed interface will be invoked and filled in with the information provided. If all four items are given via the command line, JawinGen will run in "silent" mode.
For example, the following command line will generate the stubs for PowerPoint (assuming the directory locations are the same on your machine):
jawinGen -l '/Program Files/Microsoft Office/Office/msppt9.olb'
-d ../../stubs/ppt -p jawin.ms.ppt -j MSPPt
After a successful run, JawinGen will produce a Java stub class for each Dispatch interface in the type library and an interface file for each enum. In addition, it will produce an Ant buildfile at the root of the destination directory. To compile your stubs and interfaces, use the command line "ant" from the root of your output directory. The .java files and interfaces will all be compiled and
JARred.
NOTE: JawinGen is currently an alpha release. Please send feedback to the Jawin mailing list or directly to justin@gehtland.com.
Calling a DLL Entry Point
In many ways, calling a DLL entry point is similar to calling a scriptable
COM object. As you recall, the class DispatchPtr represented a
scriptable COM object. The class FuncPtr represents a DLL entry
point, and you create a FuncPtr by specifying a library name and
entry point name. The following snippet gets the MessageBoxW method
and uses it to show a message box.
//ant -emacs "hello dll"
FuncPtr msgBox = new FuncPtr("USER32.DLL", "MessageBoxW");
msgBox.invoke(0, "Hello From a DLL", "From Jawin", 0, ReturnFlags.FAIL_ON_FALSE);
The first four arguments to invoke are the arguments expected by MessageBox:
int, String, String, and int.
FuncPtr has several different overloaded forms for common argument
types. The final argument is a ReturnFlags that tells Jawin how to
process the return value. DLL entry points have several different ways to
indicate errors. Some return false (0) on failure, others return false
on success. Still others return a COM HRESULT, or indicate failure
through a call to the helper function GetLastError.
To call MessageBoxW using a FuncPtr, you have to
know the argument types in advance, and you have to know how MessageBoxW
indicates failure. What happens if you pass the wrong arguments? The most likely
outcome is that your program will crash. If you are familiar with Visual
Basic, this problem is equivalent to using the wrong Declare statement. An API
that crashes on bad input is less than ideal, so Jawin also includes
strongly-typed stubs that hide the details of calling invoke behind a type-safe
wrapper. Using the MessageBoxW stub, the above code would be:
//ant -emacs "hello dll stub"
User32.MessageBoxW("Hello From a DLL Stub",
"Jawin");
This is much safer, since there are no overloaded forms of User32.MessageBoxW
that might take the wrong arguments and crash. At this time, Jawin does not
include an automatic stub generator for DLL entry points; volunteers code the
stubs by hand. If you have an API that you need wrapped in a stub, post a
description to the mailing list.
Jawin Architecture
One goal of any interop tool is transparency. Ideally, a Java programmer could use a COM or Win32 component without even knowing she was doing so. The component would behave just like any ordinary Java class, and the presence of Win32/COM would be entirely transparent to the programmer.
In reality, some details cannot be entirely hidden, so
the next best thing is to have translucent access. A translucent stub is a Java
class that hides most of the details of COM and Win32. The code below
shows translucent stub code for accessing the Windows registry from Java. In
this example. the methods have simple signatures that do not cause any
particular challenges for a marshalling layer. As a result, the only departure
from transparency is the presence of COMExceptions.
public class Registry {
public static int OpenKey(int key, String subkey)
throws COMException;
public static int CreateKey(int key, String subkey)
throws COMException;
public static void DeleteKey(int key, String subkey)
throws COMException;
public static String QueryStringValue(int key, String subkey)
throws COMException;
public static byte[] RawQueryValue(int key, String subkey)
throws COMException;
public static void CloseKey(int key)
throws COMException;
}
In order to produce stubs like the one shown above, you need three things:
-
Type information that describes the entry points or interfaces to be accessed.
-
Intrinsic functions (helper functions that marshal particular data types).
-
A marshalling layer that assembles calls to the intrinsic functions based on the type information.
Here is how Jawin does it (click to view diagram).
The translucent stub is generated from type information, and is different for each COM interface or DLL entry point. Shared stubs handle common method signatures by calling native methods with correlated JNI signatures. The generic stub handles "everything else", i.e. methods that have less common signatures and therefore no shared stub. Both the generic stub and shared stubs use helper functions (called intrinsic functions) to marshal particular data types.
Jawin Intrinsics
Jawin intrinsic functions are the atoms of marshalling. An intrinsic function knows how to convert one or more data types into a wire format, or how to retrieve data types from a wire format.
On the Java side of Jawin, the intrinsics are located in the com.develop.io
package and the com.develop.jawin.Variant class. LittleEndianInputStream
and LittleEndianOutputStream know how to handle Java primitives,
e.g.:
package com.develop.io;
public class LittleEndianInputStream {
public final int readUnsignedShort() throws IOException {
InputStream in = this.in;
int ch2 = in.read();
int ch1 = in.read();
if ((ch1 | ch2) < 0)
throw new EOFException();
return (ch1 << 8) + (ch2 << 0);
}
//etc.
}
As the class names suggest, Jawin marshalling always uses little endian byte order, which is the ordering expected by Win32 and COM. This strategy could be called "Java makes right," since all the byte-ordering work is done in Java, both during method calls and returns. I chose this approach because I believe that Java code is easier to write and test and equivalent COM code.
In addition to handling data types, intrinsic functions also handle other semantic conversions. For example, there is a native intrinsic function that converts Win32/COM return values into Java exceptions:
#define CHECK_NOTHING 0
#define CHECK_FOR_FALSE 1
#define CHECK_FOR_FAILED_HR 2
inline bool checkRet(int ret, int flags) {
switch (flags) {
case CHECK_NOTHING:
return true;
case CHECK_FOR_FALSE:
if (!ret) {
JNIComException::SetLastError();
return false;
}
return true;
case CHECK_FOR_FAILED_HR:
if (FAILED(ret)) {
JNIComException::SetContextException(ret);
return false;
}
return true;
//etc.
}
}
On the native side of Jawin, the intrinsic functions are spread across
SharedStubs.cpp, GenericStub.cpp, and Transform.cpp. At some future date they
may be factored into an Intrinsics.cpp.
Shared Stubs
Jawin uses shared stubs to marshal COM and Win32 methods that have common signatures. For example, consider the following Win32 function calls:
HGDIOBJ GetStockObject(int fnObject);
HRESULT CoInitialize(LPVOID reserved);
BOOL UpdateWindow(HWND hwnd);
BOOL DeregisterEventSource(HANDLE hEventLog);
Semantically, the argument types and return values of these methods are all very different. However, all these types marshal the same, as 32-bit values. Thus, it is possible to implement all of these methods with a single JNI entry point:
public static native int invokeI_I(int arg0, int func, int flags);
This greatly reduces the number of entry points that must be implemented to
handle a given set of APIs. Instead of one entry point per method, you need only
one entry point per unique signature. Given this "shared stub," an
implementation of CoInitialize looks like this:
public static void CoInitialize()
throws COMException
{
FuncPtr fp = new FuncPtr("OLE32.DLL",
"CoInitialize");
fp.invoke(0, ReturnFlags.CHECK_HRESULT);
}
Jawin's shared stubs are in the Java class com.develop.jawin.marshal.SharedStubs.
The native implementation is in the files SharedStubs.cpp and COMMarshal.cpp.
Of course, not all functions can be implemented with a finite number of shared stubs. For methods with more exotic signatures, Jawin also provides a generic stub.
Jawin Generic Stub
Jawin's generic stub is a true marshaller, similar to RMI or DCOM. The generic stub views a function call as a sequence of events, as shown in the figure here. Intrinsic functions serialize a function into a request message, and the generic stub moves this message into native space. There, another set of intrinsic functions convert the serialized request into a call stack and invoke the function. The entire sequence plays backwards to serialize a response message with return values or exceptions and ship it back to the caller.
The generic stub knows how to send messages back and forth from Java to Win32, but it does not know the specifics of any particular method. On the Java side, the translucent stub knows these details. However, there is no Win32-side equivalent of the translucent stub. Sticking to the RMI and CORBA naming convention, such a component would be called a skeleton. Jawin does not use a skeleton, because the request message carries with it the type information.
In addition to the serialized request, the generic stub also passes an instruction stream that describes how to deserialize the request on the Win32/COM side. The instruction stream is sequence of bytecodes that is processed by a simple interpreter to rebuild the call stack. These are not bytecodes in the sense of a Java binary class; they are Jawin-specific and arbitrary.
The figure below shows the instruction stream that is
generated when marshalling a call to the Win32 API MessageBoxW. The instruction stream begins with
"0,0,0,0" for the first argument, which happens to be an integer
valued zero. The second argument is a string, which is a little more complex.
The "5,0,0,0" is the little endian representation of the string
length, followed by the string's contents, encoded as Unicode characters. The
remainder of the request (not shown in the figure) is generated in similar
fashion.

The code "kGGk" is the instruction
stream that tells how to rebuild the call stack. The "k" code
indicates that a 32 bit value should be copied directly into the stack. The
"G" code indicates that the stub should allocate a Unicode string,
read the stream's contents into the string, and then place a pointer to the
string on the call stack. These instruction codes are only a sample; Jawin's
instruction-string vocabulary supports several stack transformation more complex
than those shown here. The instruction strings drive a state machine that is
implemented as a switch statement in the Transform::process
function.
Jawin's generic stub is primitive compared to RMI or DCOM. For example, it does not understand pointer aliasing. Improvements are being made the generic stub on an as-needed basis to support particular use cases.
Error Handling
Jawin automatically converts COM and Win32 errors into instances of com.develop.jawin.COMException.
For COM errors, the exception will include the HRESULT and the
error string. For example, the following fragment attempts to create a
non-existent COM component:
//from demos/src/demos/BadHresult.java
try {
Ole32.CoInitialize();
DispatchPtr p = new DispatchPtr("new:Nonexistent.Component");
throw new Error("Attempt to create nonexistent component should fail");
} catch (COMException e) {
System.out.println("Got Expected Error: " + e);
}
The preceding example will produce the following COMException:
> ant -emacs "demo bad HRESULT"
> Got Expected Error: com.develop.jawin.COMException: 800401e4: Invalid syntax
Often, the HRESULT is inadequate to diagnose an error. In such
situations, it is necessary to also collect any additional thread-specific error
information set by the object. As an example of this, consider the ADO demo
included with Jawin (see demos/src/jawin/ado). The Ant build target "ado
demo" passes in the data source name "DSN=Pubs".
If this data source name does not exist, you will see an error like this one:
com.develop.jawin.COMException: 80020009:
[Microsoft][ODBC Driver Manager] Data source name not found and no default
driver specified
[src=Microsoft OLE DB Provider for ODBC Drivers,
guid={0C733A8B-2A1C-11CE-ADE5-00AA0044773D}]
Without the additional information provided by SetErrorInfo, you
would see only the HRESULT and the generic information Exception
Occurred.
For Win32 errors, the COMException will include the error code
and text reported by calling GetLastError.
COM Threading
Unlike Java or Win32, COM provides some built-in protection for components
that can only be called safely from certain threads. The architecture to support
this is based around apartments. An apartment is a group of running
components and threads with similar threading characteristics. The relationship
between a COM component and the apartment model is specified at deployment by
setting well-known values in the registry, but can also be modified at runtime
by implementing the IMarshal interface. There are a confusing
variety of possibilities:
- A component can live in a single-threaded apartment (STA), which means that it should always be called from the same thread.
- A component can live in the multi-threaded apartment (MTA), which means that it can be called from any thread in the MTA pool, but can never be called from an STA thread.
- Components can also choose the "Both" threading model, which means that they will belong to the apartment of the thread that creates them.
- Components can implement the
IMarshalinterface to further customize threading behavior at runtime, adding additional nuances to the apartment behavior suggested by their registry settings. This feature is commonly used to create components that can visit any apartment for the duration of a method call. Such components may be called "agile" or be said to "aggregate the free-threaded marshaller." - In MTS and COM+, the threading model is extended to also include the notion of context. A context is a subspace of a process that provides some service at runtime, such as security checking or transaction enlistment. Components may belong to the same apartment but still be incompatible if they belong to different contexts.
If you are a Java programmer, and you think that the preceding section sounds complex, bewildering, and likely to cause trouble, you are right! (For the full story on apartments and context, see Tim Ewald, "Transactional COM+: Building Scalable Applications", Addison-Wesley, Reading, MA, 2001.) To summarize the apartment story: Apartments are complex, and if you call a COM apartment from the wrong thread you may violate apartment rules and cause bizarre failures far removed from the problem point in the code.
The Jawin architecture provides two levels of service for Java programmers:
- For Java programmers who are not experts on COM apartments, the details of COM threading are hidden as much as is feasible.
- For Java programmers who are expert on COM apartments, full functionality should be available.
The Jawin architecture accomplishes these objectives by mandating the following programming model:
When you create or otherwise gain access to a
COMPtr,DispatchPtr, or any subclass, you should only use that pointer from the thread you are on. When you are done, you must call the close method, which will release the underlyingIUnknown*. This approach is recommended for anyone calling COM objects from a single thread.If you wish to use a
COMPtr,DispatchPtr, or subclass from more than one thread, you should callIdentityManager.createGITRefto create a context-neutral reference in the Global Interface Table. After you do this, you can use the newly created reference from any thread, and Jawin will automatically hide the details of creating a localIUnknown*as needed. While this approach will always work, it significantly increases the overhead imposed by Jawin on each method call. This approach is recommended for Java programmers who plan to use the same COM object from multiple threads, and are not comfortable with the details of COM apartments.You can improve the performance of the second option by calling
IdentityManager.createDirectRefto create a thread-local reference just before making a series of method calls. This direct reference must be closed on the thread where it was created. This option is recommended for Java programmers who are comfortable with the details of COM apartments.
Troubleshooting Jawin
If you are getting an UnsatisfiedLinkError when loading Jawin, read the
section Loading Jawin Native Code. If some stubs or
data types cause exceptions, you may be able to track the problem using Jawin's Diagnostic
Output. If this documentation does not solve the problem, post your question
to the mailing list.
Loading Jawin Native Code
When you use Jawin to access native code, you need only load the single Jawin
library. No other native code is necessary. However, Jawin itself is a standard
JNI library. The first challenge in using Jawin is making sure that the Jawin
library is visible to your application. You can set the standard java.library.path
property to point to the location of jawin.dll on your system:
java -Djava.library.path=d:/jawin/bin SomeMainClass
Jawin also includes a special flag that you can use to specify the full path and name of the Jawin library. This form is usually used to load the debug version of the Jawin native code, like so:
java -Dcom.develop.jawin.hardlib=d:/jawin/bin/jawind.dll
SomeMainClass
The debug version of Jawin is located at bin/jawind.dll in your Jawin
installation. The purpose of the debug build is to help troubleshoot problems
inside Jawin. The debug version of Jawin contains debug symbols and may
sometimes ASSERT before throwing a COMException back
to Java. This is by design, so that you can jump into the debugger to see what caused the exception. You should
never deploy the debug version of Jawin.
You can also access the Jawin library by adding it to your Windows system directory, or by running your application from the directory where the library is located. These methods may seem simpler, but they can cause configuration headaches later. The explicit command-line flags listed above are the preferred way to locate the Jawin native library.
Diagnostic Output
Jawin includes several auditing flags that can be used to emit diagnostic output, as described in the table below. The diagnostic output will be enabled if these flags are set to any value.
| Flag Name | Diagnostic Output |
com.develop.jawin.traceRefs |
COM object reference counting |
com.develop.jawin.traceWin32 |
Win32 marshal packets |
com.develop.jawin.traceCom |
COM marshal packets |
com.develop.jawin.traceDispatch |
Scriptable COM object marshal packets |
This listing shows the diagnostic
output from running the PowerPoint demo with com.develop.jawin.traceDispatch
enabled. As you can see, the information is extensive, including a binary log of
all data transmitted between Java and native code.
Resources
This introduction to Jawin assumes that a reader is already somewhat familiar with COM, Win32, and Java.
- For COM platform basics, see Don Box, Essential COM, Addison-Wesley, Reading, MS 1998.
- For Win32, see Programming Applications for Microsoft Windows, 4th Ed., Microsoft Press, Redmond, WA, 1999.
- For Java platform basics, see Stuart Halloway, Component Development for the Java Platform, Addison-Wesley, Reading, MA, 2001, especially the appendix on Jawin.
- Jawin is one of a large number of open-source and commerical products that integrate Java with COM and Win32. For a list of alternatives, see Java/Win32/COM interoperation product list.
Please send comments and questions to the Jawin mailing list. You can subscribe on the Web at http://discuss.develop.com/jawin.html.
Stuart Halloway is a co-founder and CEO of Relevance, Inc. Relevance provides development, consulting, and training services based around agile methods and leading-edge technologies such as Ruby and Clojure.
Return to ONJava.com.