Friday, May 1, 2009

My new favorite Sentence in "Black Swan"

"If you hear a 'prominent' economist using the word equilibrium or normal distribution don't argue with him; just ignore him or try to put a rat down his shirt."

webMethods IData => java.util.Map => IData

I am not a fan of webMethods IData construct. Working with IData is needlessly obtuse, in my opinion. For example, I have an IData object nested inside another IData object, and I want to access values in the child IData object:

IData top = someObj.callServiceWithResultsPutInIData(params);
IDataCursor t_curs = top.getCursor();
IData child = IDataUtil.getIData(t_curs,"ChildKey");
IDataCursor c_curs = child.getCursor(child);
String val = IDataUtil.get(c_curs,"Key");


Thats alot of code to access a value in a nested IData object. I know, however, what the cursor is. Its really just an external iterator for IData objects. IData is really just a collection, but it doesn't implement Java's collection interface. Unlike most collections (think lists, arrays, trees, vectors, etc.) IData forces you to use the cursor in order to access its elements, this leads to ugly, inefficient code like what you see above.

Besides ugly code, however, there is something much more sinister at play here. When interacting with WM services from within java code, developers are tempted to sprinkle references to IData throughout their code. From a strategic point of view, this gives webMethods (actually, Software AG, the company that owns webMethods) lock in by raising our switching costs. The further up your application stack you allow the IData tentacles to stretch, the higher these switching costs become, and more and more "bargaining power of suppliers" is given to Software AG. In my opinion, this is bad.

Again, I hate working with IData. But, I have a solution and it is exceptionally easy: convert IData to java.util.Map. Mathematically, the two structures are identical - anything you can represent in IData can be represented in Map and vice versa. A nested IData object is really just a Tree, and you can make a Tree out of a Map. Within my method to do the conversion I use the concrete class HashMap, but this will work with anything that satisfies the Map interface. Here is how to convert IData to Map:


public Map convertIDataDocToHash(IData input)
throws DataFormatException{
HashMap output = new HashMap();
IDataCursor i_curs = input.getCursor();
String key = "";
Object o = null;
while(i_curs.next() != false){
key = i_curs.getKey();
o = i_curs.getValue();
if(o instanceof IData){
output.put(key, convertIDataDocToHash((IData)o));
}else{
if(!(o instanceof String)){
throw new DataFormatException("Scalar value in Hash is not a
String, all scalar values need to be Strings.");
}
output.put(key,o);
}

}

i_curs.destroy();
return output;
}


Using a pretty simple recursion, I can convert any IData document of arbitray depth and width into a HashMap of the same dimension. Since the two structures are identical, the same algorithm works to convert HashMap (or any Map object) to IData:


public IData convertHashToIDataDocument(Map m_data)
throws DataFormatException{
//create a new toplevel IData object
Set key_set = m_data.keySet();
IData inputs = IDataFactory.create();
IDataCursor i_cur = inputs.getCursor();
String key = "";
for(Iterator it = key_set.iterator(); it.hasNext();){
key = (String)it.next();
Object o = m_data.get(key);
if(o instanceof Map){
IDataUtil.put(i_cur,key , convertHashToIDataDocument((Map)o));
}else{
//this needs to be a string since we are working with a document
if(!(o instanceof String)){
throw new DataFormatException("Scalar value in Hash is not a
String, all scalar values need to be Strings.");
}
IDataUtil.put(i_cur, key, (String)o);

}
}
i_cur.destroy();
return inputs;

}


So, now, I can call a service and use Java collections in my business critical code, instead of IData. Also, when I invoke the service, I can pass a HashMap to a wrapper method that will call the service for me, converting the HashMap to IData on the fly:


public HashMap runService(String url, String user, String pass,
String serv, HashMap inputs){
//Establish the context
Context cntxt = new Context();
System.out.println("Context Established....");
try {
//make the connection
cntxt.connect(url, user, pass);
System.out.println("Connection Established with Integration Server....");
} catch (Exception e) {
System.exit(0);
}
IData id_input = null;
try {
id_input = this.convertHashToIDataDocument(inputs);
} catch (DataFormatException e1) {
e1.printStackTrace();
}
IData output = null;
try {
output = cntxt.invoke(serv.split("\\:")[0],
serv.split("\\:")[1], id_input);
} catch (Exception e) {
e.printStackTrace();
}

//convert the IData doc back to a hash
try {
return (HashMap)this.convertIDataDocToHash(output);
} catch (DataFormatException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
}
}


Now that I have everything wrapped in Map, strategically I am back on solid ground and my code to access the data returned by the service is much cleaner:

HashMap inputs = this.buildInputs();
HashMap m = client.runService(url,user,pass,service,inputs)
//Get a top level scalar value:
String val1 = m.get("Top_Key_1");
//get a child hash, and access a value
HashMap child = (HashMap)m.get("child_1");
String val2 = child.get("Child_Key_1");


Thats much nicer. So, if you are going to be calling WM services with client code outside of the IntegrationServer, do yourself a big favor and use a pattern like this to push the WM specific code as far down into the application stack as possible.