Hi Tobias,
I have now ported around 80%-90% of the MS Outlook object model (over 100 classes) to your COM framework. Having also previously completed a similar exercise using the eclipse swt COM framework and having dabled with several other similar frameworks (such as Jacob) I feel able to speak with some authority on ease of use and what works/what doesn't work.
Overall my impressions are very favourable, especially given that this is still an embryonic piece of work. I especially like the fact that it is part of the JNA project. This absolutely feels like the right place for it.
Having used it quite a bit now I feel comfortable saying that the following three things would turn a really good first effort in to a solid and simple to use framework (I'll list them then explain each in detail):
1. Minor changes to the way the programmer interacts with the VARIANT class;
2. A few extra convenience methods in the COMObject base class;
3. A MockIDispatchObject class to implement the IDispatch interface for unit testing purposes.
1. Changes to the VARIANT class:
================================
When implementing a set of real world COM wrappers (such as for the MS Office object model) it is typically necessary to create and use many hundreds of instances of VARIANT for parameter passing to and from COM properties and methods. It would make life so much easier if:
* there were constructors accepting all the basic Java types (short, int, String, Date, IDispatch, etc). My work around has been to provide my own base class between COMObject and my actual wrappers and provide it with methods having prototypes like:
VARIANT newVariant(short val);
VARIANT newVariant(String val);
VARIANT newVariant(java.util.Date val);
* there were methods on the class for extracting the same Java type values:
var.shortValue();
var.intValue();
var.stringValue();
var.dateValue();
etc.
I found no work round for having to manually navigate the getValue() returned intermediate object and it was a real pain point. I know that platform portability is of prime concern to the JNA project but... as far as I am aware 'COM' is only pertinent to Windows so this should not be a problem.
2. COMObject convenience methods:
=================================
I estimate that well over 99% of the times that I wanted to interact with the underlying IDispatch methods/properties it was for the same IDispatch as stored within COMObject. That coupled with the need to create a return VARIANT object for every call makes directly using the oleMethod() calls overly verbose and complex.
As a work around I took a leaf out of the swt COM framework book and added simplified invoke() and invokeNoReply() methods to my base class that inherited from COMObject. The prototype for these methods being of the form:
VARIANT invoke(String methodName);
VARIANT invoke(String methodName, VARIANT[] args);
VARIANT invoke(String methodName, VARIANT arg1);
VARIANT invoke(String methodName, VARIANT arg1, VARIANT arg2);
etc.
I did a similar thing for methods that returned no value:
void invokeNoReply(String methodName);
void invokeNoReply(String methodName, VARIANT[] args);
void invokeNoReply(String methodName, VARIANT arg1);
void invokeNoReply(String methodName, VARIANT arg1, VARIANT arg2);
etc.
Finally I provided generic property getters and setters that took/returned Java types directly. For example:
short getShortProperty(String propertyName);
void setProperty(short value);
int getIntProperty(String propertyName);
void setProperty(int value);
String getStringProperty(String propertyName);
void setProperty(String text);
java.util.Date getDateProperty(String propertyName);
void setProperty(java.util.Date date);
IDispatch getIDispatchProperty(String propertyName);
void setProperty(IDispatch iDisp);
By providing these inherited methods (ideally at the COMObject level) the implementation code for the typical wrapper method drops to around 1 simple line of code. This is very important when there are typically many hundreds of these methods to implement. A typical method implementation then takes the simple and elegant form:
public Date getCreationTime() {
return getDateProperty("CreationTime");
}
public void resetForm() {
invokeNoReply("ResetForm");
}
3. MockIDispatchObject class
============================
This is not unique to your framework, though yours provides a nice simple mechanism for implementing unit tests that mock up the dependency on the underlying third party application. Let me explain.
Historically I have been a really bad boy when it comes to creating unit tests for desktop user applications (I normally work on server-side applications with no user interface). However, the sensible project policy for JNA of not accepting code without unit tests got me thinking. I can get away with depending on things like Outlook while it is on my PC, but that is unacceptable for a central (automated?) build system.
Initially I was thinking that I would need to mock all my application classes using something like 'mockrunner'. However, after a bit of thought I realised that due to the way that the JNA COM framework works, the problem was very generic and relatively easily solved.
With one exception all my COM wrappers are created by passing an IDispatch object in to the constructor (and the exception, initial creation of the application object, also relied on the same thing - it just obtained it in a different way). So all we need is a single generic MockIDispatchObject class which I suggest would have a public interface something like this:
public class MockIDispatchObject implements IDispatch {
// Called by unit test to setup the object to accept the method/property call.
// Should be able to register multiple calls to allow testing of applications
// based on the wrappers rather than just the wrappers.
void registerMethodCall(String methodName, int methodType, VARIANT[] args, VARIANT returnValue);
// May be also simplified variations with between zero and four arg parameters.
// Check how many arguments were actually passed in the test
int getActualArgumentCount();
boolean assertAllArgumentsCorrect();
// Check that the given argument number had the expected values in the test
boolean assertActualArgumentValueCorrect(int argIndex);
// If argument value assertion fails then retrieve the value actually passed in
VARIANT getActualArgument(int argIndex);
// Then the actual interface methods would be implemented to use the registered values
}
I imagine that a typical unit test case would then look something like this:
public class TestOutlook extends TestCase {
private MockIDispatchObject iDisp;
private SomeComWrapper comWrapper;
private void setUp() {
iDisp = new MockIDispatchObject();
comWrapper = new SomeComWrapper(iDisp);
}
public void testSomeMethod() {
VARIANT expectedReturn = new VARIANT(expectedReturnValue);
VARIANT[] args = { new VARIANT(arg1Val), new VARIANT(arg2Val) };
iDisp.registerMethodCall("SomeMethod", OleAuto.DISPATCH_METHOD, args, expectedReturn);
int result = comWrapper.someMethod(arg1Val, arg2Val);
// Normal assertion that arguments and result were as expected
assertTrue("At least 1 actual argument was wrong", iDisp.assertAllArgumentsCorrect());
assertEquals("Unexpected result", expectedReturnValue, result);
// Further debugging if iDisp.assertAllArgumentsCorrect() was false
// assertEquals(2, iDisp.getActualArgumentCount());
// assertTrue("1st Argument value incorrect", iDisp.assertActualArgumentValueCorrect(0));
// assertTrue("2nd Argument value incorrect", iDisp.assertActualArgumentValueCorrect(1));
}
}
Oh. I have not forgotten about donating my Outlook wrappers to your project. However, without tests it is not possible right now. So if I can figure out GitHub I will temporarily post it to my own site and then post the link here so people can get an advanced peek.
Regards,
Ian Darby