Saturday, February 2, 2013

XPages: Detecting and Logging Field Value Changes

Recently I had a request to add functionality to an XPages application I wrote and implemented last year.  They wanted the ability to log changes made in specific fields.  I did some searching and found the resources I needed:

valueChangeListener code as a managed bean by Tony McGuckin, which was linked from
valueChangeListener implementation code from Declan Sciolla-Lynch, which pointed to
A better way to update log fields by Chris Toohey

I used my "R&D skills" to set up the code in my application and was excited when it seemed to work right off the bat.  I moved it to the QA environment and then things started going bad.  I kept getting Null Pointer Exception errors when I tried to add and update documents.  I eventually realized a) I hadn't tried to create documents on the development server, and b) I hadn't tried to update documents that had never been updated previously.  Neither the valueChangeListener bean nor the addToList function were working if the starting value of the field was an empty string.

I still need to figure out how to identify an empty string in those fields to properly handle that situation, but I got around it by making a few changes in the code and by adding a default value to a field so it doesn't start as blank.

The "do something useful" code to put in Tony's bean ('c' is a UIComponent object):
// Get the name of the field that changed
String changedExpression = c.getValueBinding("value").getExpressionString();
int firstDot = changedExpression.indexOf(".") + 1;
int closingBracket = changedExpression.indexOf("}");
String changedField = changedExpression.substring(firstDot,closingBracket);
      
// Get the old and new values in the field
String oldValue;
try {
    oldValue = valueChangeEvent.getOldValue().toString();
} catch (NullPointerException npex) {
    oldValue = "";
} catch (Exception ex) {
    System.out.println("Major failure");
    ex.printStackTrace();
    oldValue = "";
};
String newValue = valueChangeEvent.getNewValue().toString();
      
Map viewScope = ExtLibUtil.getViewScope();
HashMap oldValuesMap = new HashMap();
HashMap newValuesMap = new HashMap();
      
if (viewScope.containsKey("oldValues")){
    oldValuesMap = (HashMap) viewScope.get("oldValues");
    newValuesMap = (HashMap) viewScope.get("newValues");
}
     
oldValuesMap.put(changedField, oldValue);
newValuesMap.put(changedField, newValue);
      
viewScope.put("oldValues", oldValuesMap);
viewScope.put("newValues", newValuesMap);
Code to read the old and new values and write them to a log field named ActivityHistory:
var dNow = @Now();
if (viewScope.containsKey("oldValues")){
    var oldValues:java.util.HashMap = viewScope.get("oldValues");
    var newValues:java.util.HashMap = viewScope.get("newValues");
    for (key in oldValues.keySet()) {
        actText = "Field " + key + " changed from '" + oldValues.get(key) + "' to '" + newValues.get(key) + "' - " + @Name("[CN]",session.getEffectiveUserName()) + " on " + @Text(dNow);;
        addToList(bundleDoc,"ActivityHistory",actText,"last");
    }
}
Updated addToList code (I used 'first' and 'last' instead of 'prepend' and 'append'):
function addToList(datadoc:NotesXspDocument, fieldname:string, newVal:string, addPos:string) {
    var newArr = new Array(datadoc.getItemValue(fieldname));
    switch (addPos) {
    case 'first':
        newArr.unshift(newVal);
        break
    case 'last':
        newArr.push(newVal);
        break
    }
    datadoc.replaceItemValue(fieldname,newArr);
    return true;
};

If anyone can explain what I missed with fields that start with a value of empty string, I'd appreciate it as I'm very early in my XPages and Java learning.  In the mean time, I hope this proves useful to others.