Tuesday, July 03, 2007

Crossing JACOB's Java COM bridge

We've used the JACOB Java COM bridge recently to overcome a few hurdles with our Exchange mailbox backup plug-in for our main product, Backup Professional, here at Attix5. And I have to send out thanks to the guys who are responsible for developing this very capable utility library.

Microsoft do not recommend backing up individual mailboxes. The reason for this is that currently there is no protocol that can do this efficiently, and Microsoft do not plan to provide one, as they feel the full Exchange backup, plus prudent mailbox management eliminates the need to backup individual mailboxes. While they do provide a utility, Exmerge, to export mailboxes to PST files, this is intended as an administration tool. But hey, people will take the path of least resistance, and as such Exmerge is used to do single mailbox backups.

The problem with Exmerge (and as far as I know, all products that do single mailbox backups) is that it uses MAPI to accomplish the export. MAPI was never intended to be an efficient high-volume backup protocol. So as a result, backups via MAPI are s..l..o..w. Furthermore, configuring MAPI on a machine generally requires extra DLL's and MAPI profile configuration. What this means for us is that we frequently ran into problems at client installations getting all of this to play nice.

All we actually need to do in our plug-in, is to retrieve a list of mailboxes for the user to select from, for backup. This might not sound like much of a task, but you'd be surprised how MAPI complicates this. For one thing, bare in mind that our client software is a Java codebase, so we have to jump through a JNI hoop to accomplish all of this. Anyway after the changes to Exchange 2000 and up, where the directory store was extended to Active Directory, I realized we could actually just query Active Directory to accomplish the same task.

Cue JACOB and Active Directory scripting.

JACOB, as the name implies, is a bridge between Java and COM. I won't elaborate further in this post, but you can find out all the dirty sordid details here. There are other Java COM bridge products out there, but I found good coverage of JACOB, and I also noticed that my favourite Java IDE, Intellij IDEA, have started using it in their product, so hey, it must be good!

I'm drawing this out a bit now, so let me cut to the chase, as the say. If you download JACOB, you'll find a samples folder, that has a very useful set of classes for use with ADO. That is what I used to do our AD scripting. This turned out to be quite an easy exercise though I did have to do my own mental translation between the ADSI scripts and the ADO class API usage.

Here's a scripting example of how to do a mailbox query:

set oConn = CreateObject("ADODB.Connection")
set oCommand = CreateObject("ADODB.Command")
set oRS = CreateObject("ADODB.Recordset")
oConn.Provider = "ADsDSOObject"
oConn.Open "Ads Provider"
set oCommand.ActiveConnection = oConn 'set the active connection
strQuery= ";(objectClass=person);adspath,name,legacyExchangeDN;subtree"
oCommand.CommandText = strQuery
set oRS = oCommand.Execute 'Execute the query
While not oRS.EOF
vObjectClass=oRS.Fields("objectClass")
// do stuff here
oRS.MoveNext
wend
This is what the Java ADO implementation translates to:

Connection conn = null;
ActiveXComponent ax = null;
try {
conn = new Connection();
conn.setProvider("ADsDSOObject");
conn.Open();
Command command = new Command();
command.setActiveConnection(conn);
command.setCommandType(CommandTypeEnum.adCmdText);
ax = new ActiveXComponent("LDAP://RootDSE");
Variant v = Dispatch.call(ax, "Get", "defaultNamingContext");
String dnc = v.toString();
String ldapStr = ";" + ldapQuery;
command.setCommandText(ldapStr);

Recordset rs = command.execute();
Fields fs = null;
try {
fs = rs.getFields();
int columns = fs.getCount();

if (rs.getEOF()) {
return list;
}

rs.MoveFirst();
String[] row;
while (!rs.getEOF()) {
row = new String[columns];
for (int i = 0; i <>fs.getCount();
i++){
Field f = fs.getItem(i);
try {
row[i] = f.getValue().toString();
} finally {
JacobUtils.safeRelease(f);
}
}
list.add(row);
rs.MoveNext();
}
} finally {
JacobUtils.safeRelease(fs);
JacobUtils.safeRelease(rs);
}
} finally {
JacobUtils.safeRelease(ax);
JacobUtils.close(conn);
JacobUtils.safeRelease(conn);
}
Quite a mouthful eh? (Disclaimer: I've ripped most of this out of my code and simplified it some for simplicity, so if you copy and paste and it doesn't compile, don't blame me. I promise you it all compiles on my side.)

The problem with this code is that if you run that query, expecting to get all (say) 2091 mailboxes in your AD forest, you'll be surprised that you'll only get 1000. That's what happened to us, but we didn't realize this until we had deployed into a live site at a client site, and had the client complaining they couldn't see all the mailboxes in certain OU's. Man, talk about a hair-pulling exercise. Yes, I know, those of you seasoned AD LDAP'ers will be going "Well d'ugh!", but you see I wasn't and still am not a seasoned AD LDAP'er.

So after running a test script on the production server, we noticed the 1000 row limit returned by the query. This was a peculiar limit. I doubted that there were exactly 1000 AD mailboxes, though I did for a moment wonder whether they had a "1000 user Exchange license". I said just for a moment, so stop laughing. Nevertheless, a Google search quickly revealed that this was actually an Active Directory search issue. Active Directory has a MaxPageSize setting, that limits any queries to 1000 rows, unless you enable paging. Google again to the rescue, and I determined that to enable paging, you simply do the following:

oCommand.Properties("Page size") = 99

This simple line tells AD to retrieve ALL possible rows. AD handles the actual paging in the background. That's it! That's the fix. Now, this sounds simple, but alas it didn't turn out to be so simple on the Java API side. I tried numerous things, using the Dispatch.get(), Dispatch.call() and Dispatch.invoke() methods of JACOB with all sorts of permutations targeting the command, or doing a command.getProperties() and setting "Page size" on that, but all attempts got me a JacobException. At the edge of my sanity, at the point where I was about to give up programming for a career sweeping the streets, I had an epiphany. Actually, I had the foresight to open Visual Studio and have a look at the API docs for the Properties returned by ADODB.Command.Properties. To my surprise, it was a read-only Properties class. What gives? Well that gave me the clue that all required properties were already there in the Command, and that I simply needed to find that property and set its value appropriately. So to cut a long story short, I came up with this:


Properties props = command.getProperties();
try {
for (int propI = 0; propI <> props.size(); propI) {
Property property = props.getItem(propI);
try {
String name = property.getName();
String value = property.getValue().toString();
if (name.equalsIgnoreCase("Page size")) {
property.setValue(new Variant(1000));
value = property.getValue().toString();
}
} finally {
JacobUtils.safeRelease(property);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JacobUtils.safeRelease(props);
}
I inserted that block just after I set the command text, and just before I execute() the command, and hey presto! It all worked like a charm. If on the 4 July, you heard an exuberant, huge shout of elation and wondered who that was, it was me. Sorry.

1 comment:

Traveler said...

Hi,

Can you share the safeRelease method from your JacobUtils class?

Thnx, Chris