OpenEdge Dynamic OpenClient Java Example
In a previous post, I said I would post an example that demonstrates the use of the OpenEdge Dynamic OpenClient. Well here is the Java version of it.
It took me a while to get this done, not so much because of the code, but because the documentation that accompanies it is pretty extensive and I have been in the middle of some significant career changes.
This post is long primarily because I walk through the highlights of the what the code does and how this bolsters what I was saying in my earlier post.
Inside the zip file are 2 zip files and 3 PDF files:
- DynamicOpenClientJar.zip is the compiled version of the code. It contains a jar file, the input.xml, a batch file, and the Progress source code.
- DynamicOpenClientJar.pdf contains setup instructions to set it up and run it.
- DynamicOpenClientSrc.zip is the source code.
- DynamicOpenClientSrc.pdf contains the instructions that that tells you how to set the source code up to work with Eclipse.
- DynamicOpenClient.pdf is a detailed tour of the code and describes how it works.
This is Sample Code
Before you dig into the code and say "Wow!! This code sucks!!" it is important to understand that this is sample code. There are a lot of things about it that are ugly. Some of the object model needs serious refining. There are cleaner ways of doing the XML parsing and it is nowhere near as defensive as it should be. There are those who believe that bad code samples lead to bad code in production, but there is another line of thought that goes that if you don't recognize those bad examples, you should not be coding to start with. The point with this code is that it demonstrates concepts and that's what it was intended to do.
Code Limitations
For the sake of brevity, I have deliberately not covered stuff like supporting supporting ProDataSets and all of the different Progress data types. I have covered the most common ones and temp-tables. ProDataSets are really an extension of temp-tables and you can feel free to go and look at the code in a generated proxy to see how they work. I also stopped short of supporting the building of temp-tables from an input XML file. The extra code to do this did not seem like it would really add that much to the example. This code example also does not support persistent procedures because they are a lot more complicated than this code will allow.
Set it up and try it
Having said all of that, I wanted to spend some time in this post elaborating on the comments that I made in the earlier post about why Dynamic OpenClient is such a powerful technology, using the example code that I am presenting here. So I am going to assume for the rest of this article that you have at least set up and run the example and that you have the source code accessible to follow along.
How it Works
The Dynamic Open Client code takes an XML file and parses it for AppServer connection parameters, R-Code signatures and calls that need to be made to the Progress/OpenEdge R-Code files. The sequence diagram at right (click on it for a full-size image) shows the process.
Once the XML file has been parsed, the utility executes each of the calls and serializes the results of the call's execution to an internal XML document. Once all the calls have been executed and serialized, the results are written out to another XML file that is specified in the input.xml file. More information on the XML file's format is in the DynamicOpenClient.pdf file.
The Main Components
To understand how this utility works, you need to understand the core of the class model. So let me start by dividing the class model into its major areas.
You can pretty much ignore the com.tsg.common project. It contains an interface - INamedItem – used by a few classes that simply has a getName() method declaration. INamedItem is implemented by some of the classes in the com.tsg.dynamicopenclient project. This interface is specifically used for objects that may be stored in a com.tsg.common.collections.OrdinalList – a specialized list that is ordinal like an ArrayList, but expects uniquely named items in the list. OrdinalList is used for things like field lists and parameter lists where the fields need to be in a specific order, but the names have to be unique.
The com.tsg.dynamicopenclient project contains three packages:
- com.tsg.dynamicopenclient - This package contains the code that does most of the work;
- com.tsg.dynamicopenclient.data - This package contains the class hierarchy for parameter and other data element definitions;
- com.tsg.dynamicopenclient.valueholders - This package contains the class hierarchy for all the value holders that are used to contain the values of input and output parameters.
Data Types
One of the most important aspects of the communication between the OpenClient and the AppServer, is the mapping of data types from Java to Progress. To make sure that this is done consistently, there is a DataType enumeration in com.tsg.dynamicopenclient. If you have not worked with Java 5, enumerations may be a new concept to you and this one is particularly more so than others. An enumeration is a great way to deal with hard-coded constants and it is particularly useful where a map of constants is necessary. In this particular case we have to map an OpenClient numeric constant to a string representation of the data type, a Java class that can be used to map to that type and a valueholder class that can be used to contain the Java type. The following definition of the INTEGER data type demonstrates the mechanism:
INTEGER (Parameter.PRO_INTEGER, // Progress numeric constant Parameter.PRO_INTEGER_NAME, // Progress character constant "java.lang.Integer", // Java class to be used to map "com.tsg.dynamicopenclient.valueholders.IntegerValueHolder"), // Dynamic OpenClient valueholder
The DataType enumeration has several methods associated with it to retrieve a constant based on the string name of the data type or to obtain the Java class, value holder class or Progress data type constant. It also contains code that will instantiate an object of either the Java class to contain the data type or the value holder. This enumeration truly is at the core of the code.
Data Elements
The com.tsg.dynamicopenclient.data package contains a set of classes that define the behavior of data elements. A data element is any item that has a name and a datatype associated with it. Thus, fields and parameters are all data elements. Data elements only store the definition of the element. They do not store the value. The value is stored in a value holder. The abstract base DataElement class contains all the behavior for parsing parameter and field definition information from an XML node.
The Parameter class extends it by providing support for parameter modes (INPUT, INPUT-OUTPUT and OUTPUT) which are mapped in an enumeration called ParameterMode in com.tsg.dynamicopenclient.
The TempTableParameter class extends the Parameter class further by providing support for parsing temp-table definitions from the source code and supporting the construction of ProDataGraphMetaData objects that are used to build the ProDataGraph to transport temp-tables between AppServer and client.
Value Holders
The com.tsg.dynamicopenclient.valueholders package contains a set of classes and an interface that are used to standardize the way that data types are dealt with. Looking at the class model, the value holder hierarchy is to the right of the diagram. This hierarchy leverages the concept of generics that was introduced in Java 5.
The IValueHolder interface is the base of the hierarchy and the ValueHolder abstract class contains the generic behavior for value holders. These classes provide support for the Progress UNKNOWN value as well as reasonable default values that match the Progress defaults for its data types.
Value holders are used to set the value of input parameters and to contain the values of output parameters.
DynamicOpenClient Class
The DynamicOpenClient class in com.tsg.dynamicopenclient contains the main() method - the entry point to the utility. It starts out by parsing the parameters to determine where the XML file is that contains the signatures and input parameters for the calls. The input.xml file's structure is detailed in the DynamicOpenClient.pdf file that accompanies the source code. In essence, though, there are 4 types of nodes that are important:
- dynamicoc – This is the root node for the document and it contains an OutputFile attribute that has the name of the file that should be used for the results of the calls.
- appserver – This node contains the signature and call nodes and a set of attributes that define the connection parameters for the AppServer.
- signature – This node contains the signature of a call that will be executed against the AppServer. The signature node may contain a set of parameter nodes which will be all of the parameters for a procedure. These parameters must be specified in the order that they are specified in the ABL source code. If the parameter's DataType attribute is TABLE, the node will contain child nodes that are the field and indexes for the table.
- call – This node contains a list of calls that need to be executed against the AppServer. Each of them should refer (by means of the Procedure attribute) to a previously specified signature. call Nodes may contain subordinate parametervalue nodes that reference a parameter in the signature by name and specify the value to be used as the input value for that parameter.
The following code extract comes from the main() method.
//Extracted from main() method.
//
//Instantiate the CallManager
CallManager manager = CallManager.getInstance();
//Parse the XML file.
try {
parse(fileName);
}
catch (Exception e) {
System.out.println(e.getMessage());
return;
}
.
.
.
Iterator<Call> iter = manager.iterator();
while (iter.hasNext()) {
Call call = iter.next(); //Get the call
call.executeCall(); //Execute the call
outRoot.appendChild(call.serializeResults(outputDocument));
iter.remove();
}
.
.
.
try {
writeResults(); // Write the XML results file out to disk.
}
catch (Exception e) {
System.out.println(e.getMessage());
return;
}
Before doing anything else, the code instantiates the Singleton CallManager to hold onto all the data that will be parsed from the XML file.
The main() method calls the static parse() method with the name of the XML file to be parsed. This method parses the XML document and iterates through the four element nodes mentioned above. It also sets up a DOM document that will be used as the output document for the results of the calls.
For each element, it instantiates an object of the appropriate class and passes it the XML node that needs to be instantiated. This object is then added to the CallManager where it will be retrieved later.
//Extracted from parse() method.
//
Node elementNode = null;
while((elementNode = iterator.nextNode()) != null) { // Loop through elements
// Handle root node.
if (elementNode.getNodeName().equalsIgnoreCase(XMLStrings.INPUT_ROOT_NODE.toString())) {
// Setup the output file.
String attrValue = XMLStrings.getAttribute(elementNode, XMLStrings.OUTPUT_FILE_ATTR.toString());
if (attrValue != null) {
outputFileName = attrValue;
}
}
// Handle appserver node
if (elementNode.getNodeName().equalsIgnoreCase(XMLStrings.APPSERVER_NODE.toString()) ) {
manager.setAppServerConnection(new AppServerConnection(elementNode));
}
// Handle signature node
if (elementNode.getNodeName().equalsIgnoreCase(XMLStrings.SIGNATURE_NODE.toString()) ) {
manager.setSignature(new Signature(elementNode));
}
// Handle call node
if (elementNode.getNodeName().equalsIgnoreCase(XMLStrings.CALL_NODE.toString()) ) {
manager.addCall(new Call(elementNode));
}
}
The CallManager implements the Iterable interface for the Call class. Back in the main() method, the next thing we do is iterate through all of the Call objects in the CallManager and we execute the call. This is where the real work happens and a description of it is below. When execution is complete, the parameter values will have been stored in the Call object and a call to serializeResults() will return an XML node that can be appended to the output document. The Call object is then deleted from the list of calls to free up the memory that any temp-tables will occupy.
Once all the calls have been executed, a call is made to writeResults() which writes the output XML document to disk.
Executing the Call
A call is actually executed by a call to executeCall() on the Call object. The executeCall() method simply builds a parameter array, calls the runProcedure() method on the AppServerConnection object, builds the output parameter value set and handles any exceptions. The thing that makes this client dynamic is the code in buildParamArray() and buildOutputValues().
buildParamArray()
In order to make a call to the AppServer, the OpenClient needs a procedure name to call and a parameter array (com.progress.openABL.javaproxy.ParamArray) for the call. The parameter array needs to contain a full description of each parameter as well as the value to be set for the parameter. The Call object has a procedure name and it has the values of the input parameters. The name of the procedure maps to a Signature object which can be obtained from the CallManager and it contains the definitions of all of the parameters that are needed for the call. Thus, buildParamArray() obtains the Signature object and builds a ParamArray object with each of the parameter definitions in the Signature object. As it attempts to add each parameter to the ParamArray, it obtains a IValueHolder object to contain the parameter's input or output parameter value. It then checks for a corresponding input value in the Call object. If it finds a corresponding value it sets the input value in the ParamArray from this value. This is how the Call information in the XML file can specify the parameter values by name in any order and only specify the ones that have values associated with them.
//Extracted from buildParamArray() method.
//
CallManager manager = CallManager.getInstance();
Signature sig = manager.getSignature(getProcedureName()); //Get the signature for this call
if (sig == null) {
throw new DynamicOpenClientException(...);
}
ParamArray params = new ParamArray(sig.getParameterCount()); //Instantiate a ParamArray object
int idx = 0;
// Iterate through all the parameters in the signature.
for (Parameter param : sig) {
// Instantiate an IValueHolder for this data type.
IValueHolder<?> valueHolder = DataType.getValueHolder(param.getDataType());
paramValues.add(valueHolder); // Put the value holder into the paramValues list.
// If we can find a parameter value that was set for this call,
// set the value of the IValueHolder to the input value.
if (inputValues.containsKey(param.getName())) {
valueHolder.setObject(inputValues.get(param.getName()));
}
Now that the data we need is available (ie, the parameter type information and input values have been set), we can go about setting the values, but we need to treat the TABLE and TABLE-HANDLE types differently from the the other data types. The reason is that they need a ProDataGraph and its metadata. The code for setting the values is therefore as follows:
Object value = null;
switch(param.getDataType()) {
case TABLE:
case TABLE_HANDLE:
TempTableParameter ttParam = (TempTableParameter) param;
ProDataGraphValueHolder graphHolder = (ProDataGraphValueHolder) valueHolder;
ProDataGraphMetaData metaData = null;
switch(param.getMode()) {
case OUTPUT:
// For a table we need the table structure. TABLE_HANDLE needs null.
if (param.getDataType() == DataType.TABLE) {
metaData = ttParam.getProDataGraphMetaData();
}
break;
default:
value = graphHolder.getProDataGraph();
metaData = graphHolder.getProDataGraph().getMetaData();
break;
}
// Add a parameter to the ParamArray with the appropriate value.
params.addParameter(idx, value, ttParam.getMode().getModeCode(),
ttParam.getDataType().getTypeCode(),
ttParam.getExtent(),
metaData);
break;
default:
// Now get the object from the IValueHolder that has the value in it.
value = valueHolder.get();
// Add a parameter to the ParamArray with the appropriate value.
params.addParameter(idx, value, param.getMode().getModeCode(),
param.getDataType().getTypeCode(),
param.getExtent(),
(ProDataGraphMetaData) null);
break;
}
The outer switch in this code handles the difference between TABLE, TABLE-HANDLE and other parameters. So skip the first cases and look at the default case (the last 11 lines of this code block from the word "default:" on. All this code does is grab the object that is in ValueHolder and assign it to the value variable. It then adds a parameter specifying the ordinal position (idx), the value, the parameter mode (INPUT, OUTPUT or INPUT-OUTPUT), the Progress DataType Code from the DataType, the extent and a null for the ProDataGraphMetaData.
The TABLE and TABLE-HANDLE cases are slightly more complicated. For a TABLE or TABLE-HANDLE if the parameter is either an INPUT or INPUT-OUTPUT parameter, the value has to be set to the ProDataGraph that will be used to contain the return value. The ProDataGraphMetaData needs to be specified to supply the table structure. In the case of an OUTPUT parameter, a TABLE parameter requires ProDataGraphMetaData but null in the value parameter and in the case of a TABLE-HANDLE, both the metadata and the value need to be null. In both cases, the Parameter object will have been specialized as a TempTableParameter object so that it could build the temp-table structure.
Once the parameters have all been added to the ParamArray, it is returned to the caller (executeCall) and is now ready to invoke the call.
runProcedure()
The runProcedure() method call is run on the AppServerConnection object that is contained in the CallManager. The AppServerConnection class performs 2 functions:
- It stores the AppServer connection parameter values; and
- It executes calls against the AppServer.
The first thing runProcedure() does is call obtainAppObject(). The OpenAppObject class provides access to AppServer connections from the connection pool and supports all communication with the AppServer. The obtainAppObject() call instantiates a Connection object and then an OpenAppObject if they have not already been instantiated. This establishes the connection to the AppServer.
Once the connection is established, the runProc() method on the OpenAppObject is run with the ParamArray that was passed in and any exceptions are thrown back to the caller. If this code completes successfully, the AppServer call has succeeded.
buildOutputValues()
After the call has completed, executeCall() calls buildOutputValues() with the ParamArray that was used in runProcedure().
//Extracted from buildOutputValues()
//
returnValue = params.getProcReturnString();
if (returnValue == null) {
returnValue = "null/Unknown (?)";
}
int idx = 0;
for (Parameter param : sig) { // Loop through each parameter in the signature.
switch (param.getMode()) { // Check the parameter mode.
case INPUT_OUTPUT:
case OUTPUT:
// Get the value of the parameter from the paramArray.
Object outputValue = params.getOutputParameter(idx);
// Set the value of the parameter in our internal array to be the result
paramValues.get(idx).setObject(outputValue);
}
idx++;
}
The buildOutputValues() method does 2 things:
- It obtains the value of the RETURN-VALUE string from Progress; and
- It loops through all OUTPUT or INPUT-OUTPUT parameters and sets the value in the IValueHolder from the value in the ParamArray.
The output parameter values are thus available to the Call object for serialization.
Why this is so powerful
That is the crux of the code in the Dynamic OpenClient Sample Utility. It's not really that complicated and although this is a crude example, it does show the relative ease with which one can build a client that invokes calls for which all signature information is obtained at runtime.
If you work through the associated document that contains a description of running the utility, you will notice how easy it is to edit the ABL code and change temp-table structures or any other signature related information, modify the corresponding signature in the XML file and re-run the utility without changing the Java code.
If you go ahead and automate the generation of the XML signatures into a post-compile operation and post it on a web-server or some other centralized resource, there is never a need to change the Java code unless you discover a bug in it or Progress adds a data type.
There's another interesting side effect to this. It would not take much effort beyond this to build an automated mechanism for dynamic WSDL generation so that you dynamically expose any ABL procedure as a WebService. Taking it further, exposing it as an EJB or J2EE container or a JMS endpoint would be fairly simple too and would mean very little would ever need to be changed in the plumbing between the client and the server. You see, as far as I am concerned, everything related to ProxyGen is plumbing that should never require a recompile unless the underpinnings change. ProxyGen forces that each time a temp-table definition is changed.
Hopefully this example is useful. I am planning a .NET equivalent later on.
loading...
4 Comments
[...] through that shows how to do the stuff. I had thought of doing a complete walk through like the Dynamic OpenClient code that I did back in November last year, but I wasn't as busy back then as I am now, and I had the time to actually write up the [...]
[...] walkthrough of some example code that actually demonstrates the functionality, like I did with the Dynamic OpenClient code late last year. GD Star Ratingloading…Exchange Web Services – Subscriptions and Notifications, 4.5 out of [...]
Hi,
Nice example of using OpenClient. I'm doing something similar but I use reflection and annotations on POJOs for the dataset/temp-table mapping. I borrowed the annotations and general architecture ideas from JPA. The big breakthrough for me was realizing I can define datasets normally in the .p files, and the parameter type to dataset-handle in Java. That lets the schema be defined dynamically on the Java side. Things will get much more complicated if I ever need to pass filled-out datasets from Java to Progress, but I've avoided that so far. Simple parameters going in, datasets coming out.
Hi Jon.
This code is derived from code a while back before JPA became commercial. Now that I have done some work with JPA myself, I can see a lot of places where this could be done so much more effectively using JPA.
I have written a parser that reads the OpenEdge R-Code (provided it is not encrypted) and produces the signature of the .p from the R-Code. The advantage is that it can construct the temp-table/ProDataSet structure on the Java side so that it can be created and passed to OpenEdge.
Regards,
Bruce