TDIing out loud, ok SDIing as well

Ramblings on the paradigm-shift that is TDI.

Wednesday, October 9, 2013

Dynamically changing Attribute Maps

One way to handle multiple Attribute Maps (either Input or Output) for a Connector during AL cycling is to do the mapping yourself via script. However, there are alternatives.

The easiest way to dynamically change a map is by defining multiple Attribute Maps under Resources in your Project and then swapping between these, for example in the Before Execute Hook of the Connector.

// first decide which map to use
// for example, "AttributeMaps/ComputerSystemMap"
// or just "ComputerSystemMap"
mapToUse = computeMapName(work)

if (mapToUse != null) {
   try {
      // true for Input Map, or false for Output
      thisConnector.useAttributeMap(mapToUse,true)
      return
   } catch (exc) {
      // report that the map is missing or invalid
   }
}


Another way is to use an externalized map file, which is a text file containing any number of lines. Each line represents a single mapping rule.


Mapping

Description

sn=
When the assignment is empty then simple mapping is used.
In the example rule shown in the left column, the attribute named 'sn' gets its value(s) from a similarly named attribute in the source Entry. For an Input Map this would be the 'sn' attribute of the conn Entry. For all other map types (Output Maps or Attribute Map components) the source will be the work Entry.
status='Updated'
Anything entered after the equal sign is considered the assignment of the mapping rule. It must be a snippet of Javascript, unless the Text with Substitution flag is used: {S}, described later in this post.

You can also reference variables or call scripted functions that are defined by the time the map rule is invoked.
In the example rule shown in the left column, the 'status' attribute will get the value specified by the Javascript snippet, resulting in the literal string value Updated


giveName=[
first = work.FirstName 

last = work.LastName 
return first + " " + last 
]
Using square brackets as shown allows the Javascript assignment to stretch over multiple lines.
You can also reference variables or call scripted functions that you have defined.
In the example mapping rule, a full name value is returned by concatenating the FirstName and LastName attributes in the Work Entry.

In addition, flags can be specified to control the behavior of a mapping rule. These flags must appear in curly braces immediately after the name of the attribute being mapped. Valid flags are: A, M and S.

The A and M flags correspond to the Add and Mod columns in the Output map of a Connector in Update mode, and control whether this attribute is mapped during Add and Modify operations. The S flag denotes that the assignment uses the TDI Text with Substitution feature, which allows for literal text values that can include one or more substitution tokens.


Mapping


Description

objectClass{A}=
The 'A' and 'M' flags are used to control when this attribute is enabled with regards to the Add and Modify operations of a Connector in Update mode. By default, attributes will be included for both Add and Modify operations unless only one is specified: A or M.
im The A flag shown in this example specifies that the 'objectClass' attribute should only be mapped for Add operations.


mail{S}={work.uid}@acme.com

The 'S' flag indicates that the following mapping rule uses Text with Substitution. As a result, any curly braces found will be replaced with the value of the substitution token specified in the braces.
For this example, the value of the uid attribute in the Work Entry is substituted for the token {work.uid} and then the literal string '@acme.com ' is appended to it. The resulting string is returned as the value for 'mail'.

Note that multiple mapping flags can combined by separating them with commas. For example: {M,S}.

Note also that in order for externalized map files to work correctly, you must apply Fixpack 3 to your TDI 7.1.1 installation.

By externalizing your map to a file, you can easily change the format of the file, or allow users of your solution to do so without having to fire up the CE. You can also swap which map file to use with the same code snippet show above - just make sure that the mapToUse variable contains the path to a valid file on disk.


Monday, September 30, 2013

Using a Connector Loop to replace Connectors in Lookup Mode - and Why

Although our motto has been 'make no assumptions', TDI makes an assumption about Connectors in Lookup mode: One and only entry will be matched successfully. Period. If none are found, you have to script (or at least enable) the On No Match Hook. If multiple are found, then the On Multiple Found Hook must be dealt with. And if you want to perform some operation repeatedly for all entries in the result set, you have to script your way through connector functions like getNextDuplicateEntry(). And you have to make sure the Lookup Limit is set high enough, since TDI will buffer this many entries in memory.



This gets simpler if you turn your Connector into a Connector Loop. In other words, Add a Connector Loop and attach your Lookup Connector to it.

A Connector Loop will not cycle at all if the search returns no entries. Otherwise it cycles once for each entry found. You control what is found each time through the Link Criteria tab of the Loop. The Loop also gives you the option to Initialize each time it starts up each AL cycle, also performing the Lookup. Or you can set the Loop to just perform the Lookup. This latter option can save time by avoiding having to set up time-consuming connections each cycle.


You can still code the On No Match and On Multiple Found Hooks if you want, but these are not mandatory for a Lookup Loop.

The Loop supports both Lookup and Iterator modes if the attached Connector supports them. In order to perform specific actions on each entry returned (like deleting them) you need to add another Connector under the Loop and set the Link Criteria to search for a unique attribute - which should be one of those returned by the Loop Input Map.

The observant reader will have noted that the Lookup Limit setting is still an issue. You solve this by changing the Mode of the Loop from Lookup mode to Iterator, since Iterator mode does not buffer entries. Then you control the search by setting the relevant search filter parameter of the Connector via the Loop's Connector Parameters tab. So for LDAP you would map the ldapSearchFilter parameter like you would any attribute, for example using Javascript.


Or you could do this using a literal Text with Substitution tokens: uid={work.EmpId}.




For for JDBC it could look like this:


Note that the names that show up in the Schema area of the Connector Parameters map are the internal names of the parameters. You see this if you click on the label for a parameter, or the pencil button out to the right of it.


Normally, a Connector gets the values from its Connection Form only when it initializes. After that point, changing a parameter value (which just changes the configuration) will have no affect. However, some Connectors refresh the value for their search criteria parameter, for example the LDAP and JDBC Connectors. As a result, you do not need to tell the Connector to re-initialize, but merely to perform the Select each time.

For those parameters that do require a re-initialization to pick up changes, just tell the Loop to Initialize each time.

It's as easy as spittin'.

Printing the stacktrace of an exception to the log

When you code your error Hooks then you generally don't get the exception stacktrace printed, as you do when the error halts your AL. Here is some handy code to do this yourself:

// Error Hook code - the error Entry will be available
var sw = java.io.StringWriter();
var pw = java.io.PrintWriter(sw);
error.exception.printStackTrace(pw);
pw.close();
task.logmsg("Stacktrace: \n" + sw)


As usual, the secret is in the Java.

Friday, September 13, 2013

JSON and XML Tutorial - Part 1

If you're working with cloud services, or probably any kind of services, you're most likely working with JSON, XML (e.g. SOAP web services) or both. Although TDI provides specific components for handling web services, there is an easier approach to deal with both formats quickly and flexibly - and in the exact same way - if you're ready to do a little scripting. This is a quick tutorial to get you started with TDI's hierarchical Entry features - or hEntry for short.

As an experienced TDIer, you know that data in the system is carried around in Entry objects. Each Entry holds zero or more named Attributes. Each Attribute contains zero or more values.  Each value is a specific Java object used to represent the actual data content of that Attribute. We'll call this a 'flat Entry', since it represents a one dimensional list of Attributes - like a directory entry or a database table.

As of version 7, the Entry now has an hierarchical mode to represent a tree of data. hEntry mode is enabled automatically when you create an Entry based on hierarchical data, for example parsing XML or JSON. You can also explicitly call the enableDOM() method of an Entry, which then makes the DOM Interface functionality active and available. Or you can just add hierarchical attributes to it.

hentry = system.newEntry() // hentry is still flat
hentry.root = null // now it has a single attribute
hentry.root.branch = null // it's become hierarchical now
hentry.root.branch.leaf = "Green" // add a node with a text value

The above script creates a tree of data in the form of an hEntry. You can get the XML representation like this:

task.logmsg(hentry.toXML())

Here is the output:

<root>
  <branch>
  <leaf>Green</leaf>
  </branch>
</root>

And produce the JSON version like this:

task.logmsg(hentry.toJSON())

Which gives you the following:

{"root":{"branch":{"leaf":"Green"}}}

At this point it might help to know a little about how DOM (the XML Document Object Model) works. Here is where I learned about it: http://www.w3schools.com/dom/

In short, the DOM Model describes how an XML document is organized into a tree structure, and the DOM Interface provides methods for reading, searching and manipulating the leaves and branches of this tree. JSON is another way of describing hierarchical data. Once you have converted hierarchical data (either XML or JSON) into an hEntry then you use the DOM Interface methods to work with the data.

The Entry object provides a couple of static methods for turning hierarchical data into an hEntry: fromJSON() and fromXML(). Since they are static methods, this means you can call them using either an existing Entry object - e.g. work.fromJSON() - or by using the Class itself: com.ibm.di.entry.Entry.fromJSON(). Since you have methods for turning an hEntry into an XML or JSON representation, this makes JSON to XML conversion as simple as:

xmlString = work.fromJSON(jsonString).toXML()

And note that the above snippet will not change the contents of work. We're just making use of the static methods.

So much for the theory. You can easily play with this using the Javascript View at the bottom of your TDI Workbench, or by firing up the Debugger. Stay tuned for the next part of this discussion.

Wednesday, June 12, 2013

Shortcuts to faster workflow

I'm an incremental developer - a habit formed by over a decade of TDI, as well as cutting my programming teeth on Turbo Pascal back in the day. To speed things up I have a number of keyboard shortcuts defined in TDI.

The main ones are:

  • Ctrl-R to run the current AL
  • Ctrl-D to debug it
  • Ctrl-DownArrow for Step Over
  • Ctrl-RightArrow for Step Into
  • Ctrl-Shift-DownArrow for Continue (to next breakpoint)
It's easy to set up. First click on the Windows menu and select Preferences. Then in the filter type keys. Select 'Keys' from the filtered list.


This gives you the keyboard shortcut editor. Now type assemblyline in the search field at the top.


Choose Debug AssemblyLine from the list, making sure it's the one for TDI, and add the shortcut. Note to select In Dialogs and Windows. Now do the same for Run AssemblyLine.

Now change the filter to step and choose Step Into and Step Over (again, making during they're for TDI operations). Finally filter on continue and bind that command.

After you press OK at the bottom of the editor dialog you are ready to quickly launch, debug and step your way to new solutions!

Monday, October 1, 2012

In Norwegian, IT is sexy

Literally. The word it actually translates to 'sex appeal'. Just had to share that.

Call a stored procedures that perform an update

Got this nifty bit of script from Janne Lahteenmaki for calling a stored procedure that performs an update, and since Janne has been sent his Metamerge pen for community contribution, it's only proper that the community receives the tribute.

In this example there is a JDBC Connector in the AL named 'connDB2':

var command = "{call LLP.FOOBARPROC (?,?,?)}";
var rs = null;
var cstmt = null;
// wrapping it all in a try-catch in case something goes awry
try {
   // Stored procedure call
   // Get open connection from JDBC Connector
   con = connDB2.getConnector().getConnection();
   // Prepare the statement
   cstmt = con.prepareCall(command);
   // Assign IN parameters (use positional placement)
   cstmt.registerOutParameter (2, java.sql.Types.INTEGER);
   cstmt.registerOutParameter (3, java.sql.Types.INTEGER);
   cstmt.setString(1, "1");
   // Make the call
   cstmt.execute();
   // Get results
   var out1 = cstmt.getInt(2);
   var out2 = cstmt.getInt(3);
   // Print them out
   task.logmsg("out1="+out1);
   task.logmsg("out2="+out2);
}
catch(e) { // oh oh - something bad happened
   task.logmsg("EXCEPTION"+e);
   var out1 = cstmt.getInt(2);
   var out2 = cstmt.getInt(3);
   task.logmsg("out1="+out1);
   task.logmsg("out2="+out2);    
}

His stored procedure looks like this:

CREATE PROCEDURE foobarproc (
   IN ikayttajatun    CHAR(8),
   -- output arvot
   OUT out_pk1 INTEGER,
   OUT out_pk2 INTEGER
)
P1: BEGIN
   -- Declare cursor
   --               ################################################################
   -- # Replace the SQL statement with your statement.
   -- #  Note: Be sure to end statements correctly (usually ';')
   -- #
   -- # The example SQL statement SELECT NAME FROM SYSIBM.SYSTABLES
   -- # returns all names from SYSIBM.SYSTABLES.
   -- ######################################################################
   -- paluukoodit
INSERT INTO TFB (ID) VALUES (ikayttajatun);
   SET out_pk1 = 0;
   SET out_pk2 = 0;

END P1

Thanks, Janne, for sharing!