Friday, October 29, 2010

LotusScript: Fast sorting of arrays using Java

If you want to sort large string arrays (>1000 items), LotusScript can be horrendously slow. Back in the day, I tried to use Java via LS2J. Unfortunately LS2J was so unstable that I went back to pure LS sorting.

The reason I'm so interested in efficiently sorting arrays, is that I use a token string array as a base when rendering various content in a CMS I maintain at work. Sorting big arrays in LS is, as said, slow. Sorting token string arrays, based on a specific token in LS is s l o w!

Then came Domino 8.5.2, and NotesAgent.runWithDocumentContext :D

Basically, it lets you send an in-memory NotesDocument to an agent (LS or Java), and modify it.

To the demoapp..

In the demoapp, I've created a couple of helpers that lets you sort arrays with the Java API (via a Java Agent). It supports (locale aware) sorting of "regular" string arrays, and token string arrays, based on a token

With 5-600 items in the array, Java beats LS, even when sorting by token. The more items, the bigger the difference.

With 10 000 items, running on my computer:
Java sort: 0.9 seconds
LS Sort (algorithm): 46 seconds
Java token sort: 1.5 seconds

Open the demoapp on the web to run the test on your system. Modify the (SortDemo) agent to test different array sizes.

>> Download DemoApp (demoapp is around 6MB, due to 40k+ documents)

Share and enjoy!

Wednesday, October 27, 2010

XPages: New methods to the Debug class

I added couple of new methods to the Debug class.

Debug.messageToPage( message:String ): Adds a message to the bottom of the page.
Example usage:
..
Debug.messageToPage( viewScope.someValue )
..
Debug.exceptionToPage( exception:String ): Adds exception message to the bottom of the page
Example usage:
function someFunction(){
try {
..
} catch( exception ){ Debug.exceptionToPage( exception ) }
}
Each call to the function results in a line being added to the "message box". The code for the Debug class can be found here.

The messages are displayed in a "validation error" type box (xspMessage).

I would think messageToPage is the most useful, as an exception may crash the entire page/the message box won't be rendered.

Share and enjoy!

Friday, October 22, 2010

XPages: Make categorized views behave

Update 22.05.14: Updated with improved code.

The current implementation of categorized views gives a table column to each column. In most cases, you probably don't want this. The code below makes the categories look more like nested sections.

The code is requires Dojo 1.4, which is bundled with Domino 8.5.2. To make it compatible with previous versions, you'd have to exchange categoryButtons.closest( 'tr' ) with code that walks up the DOM tree until it finds a tr-node.

function transformCategorizedViews(){
 // Needed for dojoj.NodeList.closest()
 dojo.require("dojo.NodeList-traverse");
  
 var dataTables = dojo.query( '.xspDataTable' );
 dataTables.forEach( function( dataTable ){
  var numColumnsTotal = dojo.query( 'thead th', dataTable ).length;
   
  var categoryButtons = dojo.query( '.xspDataTable button[title~=collapsed], .xspDataTable button[title~=expanded]' );
  
  // Get parent row/find all the empty cells before expand/collapse button
  var categoryButtonRows = categoryButtons.closest( 'tr' );
  categoryButtonRows.forEach( function( categoryButtonRow ){
   var numCellsWithContent = 0;
   var numEmptyCellsBeforeButton = 0;
   var cells = categoryButtonRow.cells;
   if( cells.length > 1 ){
    var categoryButtonCell = null;
    for( var i = 0, numCells = cells.length; i < numCells; i++ ){
     var cell = cells[i];
     var cellIsEmpty = ( cell.innerHTML === '' );
     
     // Hide empty cells/set colspan to the category equal to hidden cells
     if( !categoryButtonCell ){
      if( cellIsEmpty ){
       numEmptyCellsBeforeButton += 1;        
      } else {
       categoryButtonCell = cell;
      }
     }
     
     if( cellIsEmpty ){
      cell.style.display = 'none';       
     } else {
      numCellsWithContent += 1;
     }
    }
     
    // colspan = column with expand/collapse + column count - num cells with content 
    categoryButtonCell.setAttribute( 'colspan', 1 + numColumnsTotal - numCellsWithContent );
    categoryButtonCell.style.paddingLeft = (30 * numEmptyCellsBeforeButton ) + 'px';     
   }
  });
 });
}

Since categorized views use partial refresh, you'll need something like my partial refresh hijacker.

Here's how to use the above code with the hijacker:
dojo.addOnLoad(function(){
 // Run on load
 transformCategorizedViews();
 // Run on partial refreshes
 dojo.subscribe( 'partialrefresh-complete', transformCategorizedViews );
});
Share and enjoy!

Tuesday, October 19, 2010

XPages: Show validation errors for multiple/specified components

There's a lot of great stuff in XPages, but there's also a few things I miss. One of those things is message-controls that you can connect to multiple fields. It's quite easy to emulate this using a little bit of SSJS, and a xp:text.

SSJS:
// Fetch messages for specified components
function getFacesMessages( components ){
try {
if( typeof components !== 'array' && typeof components != 'object' ){ components = [ components ]; }
var clientId, component, messages = [], msgIterator;
for( var i = 0; i < components.length; i++ ){
component = components[i];
if( typeof component === 'string' ){
clientId = getClientId( component );
} else {
clientId = component.getClientId( facesContext );
}

msgIterator = facesContext.getMessages( clientId );
if( !msgIterator ){ continue; }
while( msgIterator.hasNext() ){
messages.push( msgIterator.next().getSummary() );
}
}
return messages;
} catch( e ){ /*Debug.logException( e );*/ }
}

XPages source code example:
<xp:text styleClass="xspMessage" 
escape="false" rendered="#{javascript:return ( this.value );}">
<xp:this.value>
<![CDATA[#{javascript:return getFacesMessages( [ 'field1', 'field2' ] ).join( '<br />' );}]]>
</xp:this.value>
</xp:text>
Share and enjoy!

Monday, October 11, 2010

XPages: Another bugfix for the partial refresh hijacker

When the onStart/-Complete/-Error code for an event was served to the browser as a string, it wouldn't run. This bug has now been fixed.

Original post with code

Thursday, October 7, 2010

XPages: Add global/field message

If you want to set a message in a xp:message/xp:messages control, here's a little code snippet for you.

// Sets a global message/message for a field
function addFacesMessage( message, component ){
try {
if( typeof component === 'string' ){
component = getComponent( component );
}

var clientId = null;
if( component ){
clientId = component.getClientId( facesContext );
}

facesContext.addMessage( clientId,
new javax.faces.application.FacesMessage( message ) );
} catch(e){ /*Debug.logException(e);*/ }
}


If the second parameter isn't specified, the message becomes global (visible in all rendered xp:messages controls on the page).

If you specify the second parameter, it has to be the component id of a field which has a xp:message control, or the field component. The message then shows up in the message control for the field.

Code stolen/ported from this page.

Share and enjoy!

Wednesday, October 6, 2010

XPages: Bug in fromJson (with fix)

Update 07.10.10: I added a by-value copy method

toJson can convert most JavaScript objects to JSON. fromJson can not convert back all JSON strings that toJson creates.

Example:
var arrayJson = toJson( [1,2,3] ); // "[1,2,3]"
fromJson( arrayJson ) // fails
If you try the same in Firefox, which has implemented the JSON API, everything works:
var arrayJson = JSON.stringify( [1,2,3] ); // "[1,2,3]"
JSON.parse( arrayJson ) // [1,2,3]
I've created a simple wrapper-class that works around this bug:
var JSON = {
// Makes a by-value copy of the object
copy: function( object ){
try {
// Faster way to copy arrays
if( object && typeof object.concat === 'function' ){ return object.concat(); }

return this.parse( this.stringify( object ) );
} catch( e ){ /*Debug.logException( e );*/ }
},

// Converts object to JSON string
stringify: function( object ){
try {
return toJson( object );
} catch( e ){ /*Debug.exception( e );*/ }
},

// Parses JSON to JS object
parse: function( JSON ){
try {
return fromJson( '{"values":' + JSON + '}' ).values;
} catch( e ){ /*Debug.exception( e );*/ }
}
}
It saddens me that there are so many "simple" bugs in the XPages API. :\

Share and enjoy!

Code snippet - Array.splice according to ECMA

I've really missed the proper Array.splice in SSJS. To avoid writing a loop for every time I want to do a "splice operation", I've made a function that should work according to spec.

// $splice -> Array.splice according to ECMA standards
function $splice( array, startIndex, numItems ){
try {
var endIndex = startIndex + numItems;
var itemsBeforeSplice = [], splicedItems = [], itemsAfterSplice = [];
for( var i = 0; i < array.length; i++ ){
if( i < startIndex ){ itemsBeforeSplice.push( array[i] ); }
if( i >= startIndex && i < endIndex ){ splicedItems.push( array[i] ); }
if( i >= endIndex ){ itemsAfterSplice.push( array[i] ); }
}

// Insert all arguments/parameters after numItems
for( i = 3; i < arguments.length; i ++ ){
itemsBeforeSplice.push( arguments[ ''+i ] );
}

// Combine before/after arrays
var remainingItems = itemsBeforeSplice.concat( itemsAfterSplice );

// Rewrite array. Arrays can't be overwritten directly in SSJS
for( i = 0, len=Math.max( array.length, remainingItems.length ); i < len; i++ ){
if( remainingItems.length > i ){
array[i] = remainingItems[i];
} else {
array.pop();
}
}

return splicedItems;
} catch(e){ /*Debug.logException( e );*/ }
}
Note! If you want to do a splice on a scoped field that's an array, you first have to copy it, or your script might crash. E.g.
viewScope.put( 'someArray', [1,2,3,4] ); 
...
var array = viewScope.someArray.concat(); // concat copies the array
$splice( array, 1, 2, 12, 22 ); // modifies the array, returns the removed items [2,3]
viewScope.put( 'someArray', array );// viewScope.someArray -> [1,12,22,4]
I really hope IBM fixes the SSJS API/makes it follow ECMA-262 Edition 3 (or newer editions), but only time will tell.