Posts Tagged: ‘8.5.3’

XPages: Execute Events with HTTP Get

30. September 2014 Posted by Sven Hasselbach

To execute an event on the server, you normally have to send a POST request, because actions will be executed in the Invoke Application phase of the JSF lifecycle. A GET request will only process the Restore View and the Render Response phase, that why you can not execute an event with a GET request.

But with the help of a PhaseListener, the execution can be done earlier in the Restore View phase:

package ch.hasselba.xpages.util;

import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import com.ibm.xsp.component.xp.XspEventHandler;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import com.ibm.xsp.util.FacesUtil;

public class ExecuteOnServerPhaseListener implements PhaseListener {

    private static final long serialVersionUID = 1L;

    public void beforePhase(PhaseEvent event) {}

    public PhaseId getPhaseId() {
        return PhaseId.RESTORE_VIEW;
    }

    public void afterPhase(PhaseEvent event) {
        
        FacesContextExImpl = FacesContextExImpl.getCurrentInstance();
        ExternalContext ec = fc.getExternalContext();
        String url = ec.getRequestPathInfo();
        String[] pathes = url.split( "/" );
        
        try{
            if( pathes.length > 2 ){
                if( "executeOnServer".equals( pathes[pathes.length -2 ] ) ){
                    String[] fullId = pathes[ pathes.length - 1 ].split(":");
                    String actionId = fullId[ fullId.length - 1 ];
                    XspEventHandler eventHandler = (XspEventHandler)
                        FacesUtil.findComponentWithFullId( fc.getViewRoot(), actionId );
                    if( eventHandler != null ){
                        eventHandler.getAction().invoke( fc, null );
                        fc.responseComplete();
                    }
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }
        
    }

}

To activate the PhaseListener, it has to be enabled in the faces-config.xml:

<?xml version="1.0" encoding="UTF-8"?>
<faces-config>
   <lifecycle>
      <phase-listener>ch.hasselba.xpages.util.ExecuteOnServerPhaseListener</phase-listener>
   </lifecycle>
</faces-config>

The following Javascript snippet extends the XSP object and adds the new function executeOnServerGet to it. The parameter is the if of the event to invoke.

XSP.executeOnServerGet = function( eventId ){
    var viewId = dojo.query('[name="$$viewid"]')[0].value;
    var url = document.forms[0].action;
    url += "/executeOnServer/" + eventId;
    url += "?$$viewid=" + viewId;
    url += "&$$ajaxid=@none";
    dojo.xhrGet({url: url});
}

When calling the function, it sends a GET request and adds the current view id to the request. With the parameter $$ajaxId set to @none, the XPages Engine is forced to send no HTML code back to the client.

And here is an example XPage:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

    <xp:eventHandler id="helloworld" event="helloworld" submit="false">
        <xp:this.action>
            <![CDATA[#{javascript:
               print("hello world " + java.lang.System.currentTimeMillis() );
            }]]>
        </xp:this.action>
    </xp:eventHandler>


    <xp:scriptBlock id="scriptBlock1">
        <xp:this.value><![CDATA[
            dojo.addOnLoad( function(){
                XSP.executeOnServerGet = function( eventId ){
                      var viewId = dojo.query('[name="$$viewid"]')[0].value;
                    var url = document.forms[0].action;
                    url += "/executeOnServer/" + eventId;
                    url += "?$$viewid=" + viewId;
                    url += "&$$ajaxid=@none";
                    dojo.xhrGet({url: url});
                  }
            });
        ]]></xp:this.value>
    </xp:scriptBlock>
    
    <xp:button value="Execute" id="button1">
        <xp:eventHandler event="onclick" submit="false">
            <xp:this.script>
                <![CDATA[XSP.executeOnServerGet( "#{id:helloworld}" );]]>
            </xp:this.script>
        </xp:eventHandler>
    </xp:button>
</xp:view>

When clicking the button, the following URL is opened in the background:

http://example.com/db.nsf/EventGet.xsp/executeOnServer/view:_id1:helloworld?$$viewid=!dwjldz64w0!&$$ajaxid=@none

A GET request was sent to the server:If you look on the server console, you will see that the Action was invoked:

XPages: Create a Database without Template

30. Mai 2014 Posted by Sven Hasselbach

On stackoverflow.com, an interessting topic was asked about how to create a notes database programmatically without using a template. The problem is, that it will not contain a Icon document. But in this document are all database properties stored. So the question is: How can you create this document?

Jesse Gallagher came up with the idea to use the DXL import and create the Icon document this way, which works fine. But the next problem is, that there is no ACL note in the database and no default view.

That’s why I modified his idea and created a Java Utility class. This class creates a new database which includes all of the required design elements.

package ch.hasselba.core;

import lotus.domino.ACL;
import lotus.domino.Base;
import lotus.domino.Database;
import lotus.domino.DbDirectory;
import lotus.domino.DxlImporter;
import lotus.domino.Session;

/**
 * DB Utilities
 * 
 * @author Sven Hasselbach
 * @version 1.1
 */
public class DBUtil {

    /**
     * creates a new database 
     * the database is identically to a database created by the -blank- template in designer.
     * The default access is set to Manager
     *  
     * @param session
     *             the session used to create the database
     * @param dbTitle
     *             the database title
     * @param dbPath
     *             the path of the database
     * @param dbServer
     *             the server 
     * 
     * @version 1.1
     */
    public static void createDatabase( Session session,  final String dbTitle, final String dbPath, final String dbServer ) {
        DbDirectory dbDir = null;
        Database db = null;
        DxlImporter importer = null;
        ACL acl = null;

        try{
            // create a new database
            dbDir = session.getDbDirectory( dbServer );
            db = dbDir.createDatabase( dbPath );

            // initialize dxl importer
            importer = session.createDxlImporter();
            importer.setDesignImportOption( DxlImporter.DXLIMPORTOPTION_REPLACE_ELSE_CREATE );

            // generate DXL
            String dxl = generateDXL( dbTitle, dbPath, db.getReplicaID() );

            // import DXL
            importer.importDxl(dxl, db);

            // set ACL: Default to Manager
            acl = db.getACL();
            acl.getFirstEntry().setLevel(ACL.LEVEL_MANAGER);
            acl.save();

        }catch(Exception e){
            e.printStackTrace();
        }finally{
            recycleObj( acl );
            recycleObj( importer );
            recycleObj( db );
            recycleObj( dbDir );
        }

    }

    /**
     * generates the DXL for a blank database
     * 
     * @param dbTitle
     *            the title of the database
     * @param dbPath
     *             the path of the database
     * @param dbReplicaId
     *             the replica of the database
     * @return    String with DXL
     * 
     * @version 1.1
     *          
     */
    private static String generateDXL( final String dbTitle, final String dbPath , final String dbReplicaId ){

        StringBuilder str = new StringBuilder();

        str.append("<?xml version='1.0' encoding='utf-8'?>");
        str.append("<!DOCTYPE database SYSTEM 'xmlschemas/domino_8_5_3.dtd'>");
        str.append("<database xmlns='http://www.lotus.com/dxl' version='8.5' maintenanceversion='3.0' ");
        str.append("replicaid='");
        str.append( dbReplicaId );
        str.append("' path='");
        str.append( dbPath );
        str.append("' title='");
        str.append( dbTitle );
        str.append("' allowstoredforms='false' ");
        str.append("usejavascriptinpages='false' increasemaxfields='true' showinopendialog='false'>");
        str.append("<databaseinfo dbid='");
        str.append( dbReplicaId );
        str.append( "' odsversion='51' ");
        str.append("numberofdocuments='0'></databaseinfo>");
        str.append("<note default='true' class='icon'>");
        str.append("<noteinfo noteid='11e'>");
        str.append("</noteinfo>");
        str.append("<item name='IconBitmap' summary='true'>");
        str.append("<rawitemdata type='6'>");
        str.append("AiAgAQAA///////wD///gAH//gAAf/wAAD/4AAAf8AAAD+AAAAfgAAAHwAAAA8AAAAPAAAADgAAA");
        str.append("AYAAAAGAAAABgAAAAYAAAAGAAAABgAAAAYAAAAHAAAADwAAAA8AAAAPgAAAH4AAAB/AAAA/4AAAf");
        str.append("/AAAP/4AAH//gAH///AP//////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAiIiIiAAAAAAAAAAAAAAI");
        str.append("jPZmZm/IgAAAAAAAAAAIjGZmZmZmZsiAAAAAAAAAjGZmZmZmZmZmyAAAAAAACMZmZmZmZmZmZmyA");
        str.append("AAAAAIxmZmZmZmZmZmZmyAAAAAjGZmZmZmZmZmZmZmyAAAAIZmbyL2byL2byL2ZmgAAAjGZmIiJm");
        str.append("IiJmIiJmZsgAAIZmZiIiZiIiZiIiZmZoAADGZmYiImYiImYiImZmbAAI9mZmIiJmIiJmIiJmZm+A");
        str.append("CGZmZiIiZiIiZiIiZmZmgAhmZmYiImYiImYiImZmZoAIZmZmIiJmIiJmIiJmZmaACGZvIiIiZiIi");
        str.append("ZiIiIvZmgAhmYiIiImYiImYiIiImZoAIZm8iIi9m8i9m8iIi9maACPZmZmZmZmZmZmZmZmZvgADG");
        str.append("ZmbyL2byL2byL2ZmbAAAj2ZmIiJmIiJmIiJmZvgAAIxmZiIiZiIiZiIiZmbIAAAI9mbyL2byL2by");
        str.append("L2ZvgAAACMZmZmZmZmZmZmZmbIAAAACMZmZmZmZmZmZmZsgAAAAACMZmZmZmZmZmZmyAAAAAAACM");
        str.append("9mZmZmZmZm/IAAAAAAAACIz2ZmZmZm/IgAAAAAAAAAAIiMZmZmyIgAAAAAAAAAAAAACIiIiIAAAA");
        str.append("AAAAUEECICABAAD/////+A4DgA==");
        str.append("</rawitemdata></item>");
        str.append("<item name='$Daos'><text>0</text></item>");
        str.append("<item name='$TITLE'><text>");
        str.append( dbTitle );
        str.append("</text></item>");
        str.append("<item name='$Flags'><text>7f</text></item>");
        str.append("<item name='$FlagsNoRefresh'><text/></item></note>");
        str.append("<view xmlns='http://www.lotus.com/dxl' version='8.5' maintenanceversion='3.0' ");
        str.append("replicaid='");
        str.append( dbReplicaId );
        str.append("' showinmenu='true' publicaccess='false' default='true' noviewformat='true'>");
        str.append("<noteinfo noteid='11a' sequence='1'></noteinfo>");
        str.append("<code event='selection'><formula>SELECT @All</formula></code>");
        str.append("<item name='$FormulaClass'><text>1</text></item></view>");
        str.append("</database>");

        return str.toString();

    }

    /**
     * recycles notes objects
     * 
     * @param obj
     *             the notes object to recycle
     */
    private static void recycleObj( Base obj ){
        try{
            if( obj != null )
                obj.recycle();
        }catch( Exception e ){}
    }
}

Here is an example XPage how to use the class:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

    DB Path:<xp:inputText id="inputTextDBPath" value="#{viewScope.dbPath}" /><xp:br />
    DB Title:<xp:inputText id="inputText1" value="#{viewScope.dbTitle}" /><xp:br />

    <xp:button value="Create DB" id="buttonCreateDB">
        <xp:eventHandler event="onclick" submit="true" refreshMode="complete">
            <xp:this.action>
                <![CDATA[#{javascript:
                    importPackage( ch.hasselba.core );
                    var dbUtil = new ch.hasselba.core.DBUtil();
                    dbUtil.createDatabase(session, viewScope.dbTitle, viewScope.dbPath, "");
                }]]>
            </xp:this.action>
        </xp:eventHandler>
    </xp:button>

</xp:view>

You can enter the database Title and the database path. After clicking the button “Create DB”, the database is created on the server (assuming you have the right to do that).

XPages: Bootstrap File Input

26. März 2014 Posted by Sven Hasselbach

When using the default file upload control in a Bootstrap application, the default file upload button does not fit anymore to the design:

To fix this issue, you can use a small jQuery plugin named Twitter Bootstrap File Input. When this plugin is added to your XPage, the button will look like this:

 

To initialize the jQuery plugin, you have to call it with a selector which selects all DOM elements of type file:

<xp:scriptBlock
    id="scriptBlockInitFile">
    <xp:this.value>
        <![CDATA[
           $(document).ready( 
               function() {
                   $('input[type=file]').bootstrapFileInput();
               }
           );
        ]]>
    </xp:this.value>
</xp:scriptBlock>

The description of the button can be changed by setting the title attribute. Additionally, you can choose if the file name will be displayed inside or outside of the button:

To place it inside, you need to add the attribute data-filename-placement to the file upload control:

<xp:fileUpload
    id="fileUploadControl"
    value="#{document.Body}"
    title="Datei auswählen">
    <xp:this.attrs>
        <xp:attr
            name="data-filename-placement"
            value="inside" />
    </xp:this.attrs>
</xp:fileUpload>

Because I have added it to a Custom Control and use it multiple times on a XPage, I have changed the original code and added a flag to prevent multiple calls, otherwise all file elements are modified over and over again:

Here is the modified code:

/*
  Bootstrap - File Input
  ======================

  This is meant to convert all file input tags into a set of elements that displays consistently in all browsers.

  Converts all
  <input type="file">
  into Bootstrap buttons
  <a>Browse</a>

  Sven Hasselbach, 26.03.2014:
  Added a fix to prevent multiple wrapping 

*/
(function($) {

$.fn.bootstrapFileInput = function() {

  this.each(function(i,elem){

    var $elem = $(elem);

    // Maybe some fields don't need to be standardized.
    if (typeof $elem.attr('data-bfi-disabled') != 'undefined') {
      return;
    }

    // --- Fix to prevent multiple wrapping 
    // Sven Hasselbach, 26.03.2014

    // check for an existing 'wrapped' attribute'
    if(!!$elem.attr('wrapped'))
        return;

    // add the 'wrapped' attribute
    $elem.attr('wrapped', 'true');
    // --- End of Fix    
    // Set the word to be displayed on the button
    var buttonWord = 'Browse';

    if (typeof $elem.attr('title') != 'undefined') {
      buttonWord = $elem.attr('title');
    }

    var className = '';

    if (!!$elem.attr('class')) {
      className = ' ' + $elem.attr('class');
    }

    // Now we're going to wrap that input field with a Bootstrap button.
    // The input will actually still be there, it will just be float above and transparent (done with the CSS).
    $elem.wrap('<a></a>').parent().prepend($('<span></span>').html(buttonWord));
  })

  // After we have found all of the file inputs let's apply a listener for tracking the mouse movement.
  // This is important because the in order to give the illusion that this is a button in FF we actually need to move the button from the file input under the cursor. Ugh.
  .promise().done( function(){

    // As the cursor moves over our new Bootstrap button we need to adjust the position of the invisible file input Browse button to be under the cursor.
    // This gives us the pointer cursor that FF denies us
    $('.file-input-wrapper').mousemove(function(cursor) {

      var input, wrapper,
        wrapperX, wrapperY,
        inputWidth, inputHeight,
        cursorX, cursorY;

      // This wrapper element (the button surround this file input)
      wrapper = $(this);
      // The invisible file input element
      input = wrapper.find("input");
      // The left-most position of the wrapper
      wrapperX = wrapper.offset().left;
      // The top-most position of the wrapper
      wrapperY = wrapper.offset().top;
      // The with of the browsers input field
      inputWidth= input.width();
      // The height of the browsers input field
      inputHeight= input.height();
      //The position of the cursor in the wrapper
      cursorX = cursor.pageX;
      cursorY = cursor.pageY;

      //The positions we are to move the invisible file input
      // The 20 at the end is an arbitrary number of pixels that we can shift the input such that cursor is not pointing at the end of the Browse button but somewhere nearer the middle
      moveInputX = cursorX - wrapperX - inputWidth + 20;
      // Slides the invisible input Browse button to be positioned middle under the cursor
      moveInputY = cursorY- wrapperY - (inputHeight/2);

      // Apply the positioning styles to actually move the invisible file input
      input.css({
        left:moveInputX,
        top:moveInputY
      });
    });

    $('body').on('change', '.file-input-wrapper input[type=file]', function(){

      var fileName;
      fileName = $(this).val();

      // Remove any previous file names
      $(this).parent().next('.file-input-name').remove();
      if (!!$(this).prop('files') && $(this).prop('files').length > 1) {
        fileName = $(this)[0].files.length+' files';
      }
      else {
        fileName = fileName.substring(fileName.lastIndexOf('\\') + 1, fileName.length);
      }

      // Don't try to show the name if there is none
      if (!fileName) {
        return;
      }

      var selectedFileNamePlacement = $(this).data('filename-placement');
      if (selectedFileNamePlacement === 'inside') {
        // Print the fileName inside
        $(this).siblings('span').html(fileName);
        $(this).attr('title', fileName);
      } else {
        // Print the fileName aside (right after the the button)
        $(this).parent().after('<span>'+fileName+'</span>');
      }
    });

  });

};

// Add the styles before the first stylesheet
// This ensures they can be easily overridden with developer styles
var cssHtml = '<style>'+
  '.file-input-wrapper { overflow: hidden; position: relative; cursor: pointer; z-index: 1; }'+
  '.file-input-wrapper input[type=file], .file-input-wrapper input[type=file]:focus, .file-input-wrapper input[type=file]:hover { position: absolute; top: 0; left: 0; cursor: pointer; opacity: 0; filter: alpha(opacity=0); z-index: 99; outline: 0; }'+
  '.file-input-name { margin-left: 8px; }'+
  '</style>';
$('link[rel=stylesheet]').eq(0).before(cssHtml);

})(jQuery);

Thanks to Gregory Pike for his good work. The jQuery plugin is distributed under Apache License.

Quick-n-Dirty: Use “isDocEditable” in an old school Java Agent

14. März 2014 Posted by Sven Hasselbach

If you want to check if a document is editable, you can do this in an old school Java agent with the NAPI function isDocEditable provided by the XSPNative class.

First you have to add the required JARs to your agent. Then, you have to call XSPNative.isDocEditable with the document you want to test:

import lotus.domino.AgentBase;
import lotus.domino.Database;
import lotus.domino.Document;
import lotus.domino.Session;

import com.ibm.domino.napi.c.xsp.XSPNative;

public class JavaAgent extends AgentBase {

    public void NotesMain() {

      try {
          Session session = getSession();
          Database db = session.getCurrentDatabase();
          Document doc = db.getAllDocuments().getFirstDocument();
          System.out.println( "Is Editable: " + XSPNative.isDocEditable( doc ) );
      } catch(Exception e) {
          e.printStackTrace();
       }
   }
}

P.S. Keep in mind that this article has been posted in the “Quick-n-Dirty” category.

XPages: Use async / defer option for external CSJS Script Libraries

10. März 2014 Posted by Sven Hasselbach

When adding CSJS libraries to your XPage, try to use the defer or the async option for a better user experience. When not using this options, the Page might be blocked during page load.

Have a look at this example XPage which contains two external CSJS scripts (for demonstration purposes they are computed to get a remote script out of nowhere):

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
    Foo!
    <xp:this.resources>
        <xp:script clientSide="true">
            <xp:this.src>
               <![CDATA[#{javascript:"http://" + @Unique() + ".null";}]]>
            </xp:this.src>
        </xp:script>
    </xp:this.resources>
    <xp:scriptBlock id="scriptBlock1">
        <xp:this.src>
           <![CDATA[#{javascript:"http://" + @Unique() + ".null";}]]>
        </xp:this.src>
    </xp:scriptBlock>
    Bar!
</xp:view>

When opening the XPage, the DOM is blocked, until the operation times out:

The best you can do is to use the async or the defer option of external CSJS scripts. For script blocks, there is an option in the DDE available:

The async option can be set with an attribute:

<xp:this.attrs>
    <xp:attr name="async" value="async" minimized="true" />
</xp:this.attrs>

To use the option for a resource, you must add them as an attribute for both options:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
    Foo!
    <xp:this.resources>
        <xp:script clientSide="true">
            <xp:this.src>
               <![CDATA[#{javascript:"http://" + @Unique() + ".null";}]]>
            </xp:this.src>
            <xp:this.attrs>
                <xp:attr name="async" value="async" minimized="true" />
            </xp:this.attrs>
        </xp:script>
    </xp:this.resources>
    <xp:scriptBlock id="scriptBlock1" defer="true">
        <xp:this.src>
           <![CDATA[#{javascript:"http://" + @Unique() + ".null";}]]>
        </xp:this.src>
    </xp:scriptBlock>
    Bar!
</xp:view>

There are some other techniques, but this is the simplest way and supported in most browsers:

XPages: Use a Method Binding as Converter

8. März 2014 Posted by Sven Hasselbach

I accidentally found a way to add a method binding as a converter to a component, because I have added a managed bean as a converter directly in the source code. The DDE does not support this.

If you go to the converter property of a component, you can only add one of the predefined converters:

But you can go to the source and add a method binding to the option, in this case my bean which implements my converter functionality.

<xp:inputText
    id="inputText1"
    value="#{sessionScope.inputText1}"
    converter="#{myConverterBean}" />

If you now reopen the saved XPage, the converter property is filled in, but cannot edited / changed anymore.

You must remove the property in the source code to get the old behaviour back.

Tested in 8.5.2, 8.5.3 & ND 9

XPages: Optimized Partial Refreshs

7. März 2014 Posted by Sven Hasselbach

Inspired by the last post of Mark, I have created a small CSJS snippet which allows to optimize the behaviour of a Partial Refresh. Normally, if you execute a Partial Refresh, all elements of a form are sent to the server. Take a look at this XPage:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

    <xp:inputText
        id="inputText01"
        value="#{sessionScope.inputText01}" />
    <xp:inputText
        id="inputText02"
        value="#{sessionScope.inputText02}" />
    <xp:inputText
        id="inputText03"
        value="#{sessionScope.inputText03}" />
    <xp:inputText
        id="inputText04"
        value="#{sessionScope.inputText04}" />
    <xp:inputText
        id="inputText05"
        value="#{sessionScope.inputText05}" />
    <xp:inputText
        id="inputText06"
        value="#{sessionScope.inputText06}" />
    <xp:inputText
        id="inputText07"
        value="#{sessionScope.inputText07}" />
    <xp:inputText
        id="inputText08"
        value="#{sessionScope.inputText08}" />
    <xp:inputText
        id="inputText09"
        value="#{sessionScope.inputText09}" />
    <xp:inputText
        id="inputText10"
        value="#{sessionScope.inputText10}" >
    </xp:inputText>

    <xp:div id="refreshMe">
        <xp:label
            value="#{javascript:java.lang.System.currentTimeMillis()}"
            id="labelNow" />
    </xp:div>

    <xp:button
        value="Normal Refresh"
        id="buttonNormal">
        <xp:eventHandler
            event="onclick"
            submit="true"
            refreshMode="partial" refreshId="refreshMe">
        </xp:eventHandler>
    </xp:button>

</xp:view>

The button refreshes only a small portion of the XPage, the DIV element refreshMe. It does not require any of the posted field values, it just refreshes the DOM element in the frontend. But when clicking the button, the posted data to the server contain all the fields and their values, which is not necessary in this case.

This is how the request looks like in Firebug (I have prefilled all fields with the values 1..9):

The response of the server is – as expected – the actual HTML code for the refreshed DOM element:

The optimized  version adds the option clearForm to partial refreshs. When using this option, only the XPages internal fields are sent to the server, but DOM will be refreshed correctly:

<xp:button
        value="Cleaned Refresh"
        id="buttonCleaned">
        <xp:eventHandler
            event="onclick"
            submit="false">
            <xp:this.script>
                <![CDATA[
                    XSP.partialRefreshPost(
                        '#{id:refreshMe}',{
                            clearForm: true,
                        }
                    );]]>
            </xp:this.script>
        </xp:eventHandler>
    </xp:button>

Now, the POST looks like this:

But wait, this is useless, you can do a XSP.partialRefreshGet instead!

That’s why there is the second option additionalFields. This allows to define all fields you want to update during the refresh:

<xp:button
        value="Cleaned Refresh"
        id="buttonCleaned">
        <xp:eventHandler
            event="onclick"
            submit="false">
            <xp:this.script>
                <![CDATA[
                    XSP.partialRefreshPost(
                        '#{id:refreshMe}',{
                            clearForm: true,
                            additionalFields: ['#{id:inputText01}',
                                              '#{id:inputText02}' ],
                        }
                    );]]>
            </xp:this.script>
        </xp:eventHandler>
    </xp:button>

When clicking the button now, the specified fields are added to the POST request (and will be updated in the JSF tree:

Here is the snippet (Tested on IE 11, Chrome 33 & FF 27 with ND9 & ND 8.5.3 )

<xp:scriptBlock id="scriptBlockPROptimized">
        <xp:this.value><![CDATA[
XSP.addOnLoad(function(){

    // hijack the existing partial refresh method
    if( !XSP.__partialRefresh ){
        XSP.__partialRefresh = XSP._partialRefresh;
    }

    // add the new one to the XSP object
    XSP._partialRefresh = function x_prfh(method, form, refreshId, options){

        // clear the form?
        if( options.clearForm ){

            // create a new HTML form...
            var newForm = document.createElement( "form" );
            newForm.setAttribute( "method", form.method );
            newForm.setAttribute( "action", form.action );

            // ... and loop all existing fields
            for( var i = 0; i<form.length; i++ ){
                var field = form[i];
                var fieldName = field.name;
                var includeField = false;

                try{

                    // check for addition fields
                    if( options.additionalFields ){
                        includeField = dojo.indexOf(options.additionalFields, fieldName)!=(-1)?true:false;
                    }

                    // only add XPages relevant fields and addtional fields
                    if( fieldName == form.id || fieldName.substr(0,2) == '$$' || includeField ){

                        var newField = null;
                        if( field.options ){
                            // special handling for fields with options
                            for( var j=0; j<field.length; j++ ){
                                if( field.options[j].selected ){
                                    newField = document.createElement( "input" );
                                    newField.setAttribute( "type", "hidden" );
                                    newField.setAttribute( "name", fieldName );
                                    newField.setAttribute( "value", field.options[j].value );
                                    newForm.appendChild( newField );
                                }
                            }
                        }else{
                            // default field handling: just clone the DOM element
                            newField = field.cloneNode( true );
                            newForm.appendChild( newField );
                        }
                    }
                }catch(e){
                    console.log(e);
                }
            }

            // call the original refresh method with the new form
            return XSP.__partialRefresh(method, newForm, refreshId, options);
        }

        XSP.__partialRefresh(method, form, refreshId, options);
    };
});]]></xp:this.value>
    </xp:scriptBlock>

Just add the script block above to your XPage, or move it into a CSJS library. But keep in mind that you have to think twice when removing data from the request. It can lead in an inconsistence between the data in the client and the data stored in the component tree on the server.

XPages: SSJS & How you can have fun at the office

4. März 2014 Posted by Sven Hasselbach

This article is a demonstration of what harmful things you can do when using the SSJS & prototyping wrong( decribed here by the great Mark Roden). Don’t do that! Especially not on a productive server!
 

Wanna have a lot of fun in the office with the other developers? Just overwrite some global SSJS functionality! They will never find out what happend to their applications!

 

This is the application we destroy

Our demonstration application uses a small Java class which has only a single method:

package ch.hasselba.xpages;

public class Demo {

    public String foo() {
       return "bar";
    }
 }

This class is used for a label on our XPage and is loaded with the importPackage functionality of SSJS:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view
    xmlns:xp="http://www.ibm.com/xsp/core">

    <xp:label
        id="labelDemo">
        <xp:this.value>    
            <![CDATA[#{javascript:
                importPackage( ch.hasselba.xpages );
                return new ch.hasselba.xpages.Demo().foo();}]]>
        </xp:this.value>
    </xp:label>

</xp:view>

When opening the XPage, nothing special happens, the label is filled with the value “bar”:

Now let’s destroy it!

Add a button on the XPage, with containing a single line of SSJS code…

<xp:button
        value="Click Me"
        id="button1">
        <xp:eventHandler
            event="onclick"
            submit="true"
            refreshMode="complete">
            <xp:this.action>
                <![CDATA[#{javascript:prototype.importPackage = null;}]]>
            </xp:this.action>
        </xp:eventHandler>
    </xp:button>

… and reopen the XPage:

Question: What will happen if you click the button?

Answer:

Exactly: The method is destroyed on the whole server! Every XPage which imports a Java package with SSJS will not work anymore. Only a complete server restart helps now. The error message you will receive won’t help you, because it looks like the Java classes could not be found. Now “help” the other developers with tips like “Maybe a build problem? Have you tried to clean the application?”.

P.S. You can execute the code on other ways, f.e, with an eval statement, or a method binding of your choice.

Tested on ND 8.5.3 & ND 9.0

XPages: Events, ActionListeners & Parameters

2. März 2014 Posted by Sven Hasselbach

To access the event parameters of an event within your actionListener, you have to access the source object of your actionEvent object:

package ch.hasselba.xpages;

import java.util.List;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.ActionEvent;
import com.ibm.xsp.complex.Parameter;
import com.ibm.xsp.component.xp.XspEventHandler;

public class MyActionListener implements javax.faces.event.ActionListener {

    public void processAction(ActionEvent event)
            throws AbortProcessingException {
        XspEventHandler eventHandler = (XspEventHandler) event.getSource();
        List<Parameter> params = eventHandler.getParameters();
        for (Parameter p : params) {
            System.out.println(p.getName() + " -> " + p.getValue());
        }
    }
}

Here is an example XPage:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

    <xp:button
        value="Label"
            id="buttonAction">
        <xp:eventHandler
            event="onclick"
            submit="true"
            immediate="true" refreshMode="norefresh">
            <xp:this.parameters>
                <xp:parameter
                    name="param"
                    value="#{javascript:java.lang.System.currentTimeMillis()}">
                </xp:parameter>
            </xp:this.parameters>

                <xp:this.actionListeners>
                    <xp:actionListener type="ch.hasselba.xpages.MyActionListener" />
                </xp:this.actionListeners>
            </xp:eventHandler>

    </xp:button>

</xp:view>

You can see the result on the console when the button is clicked:

When doing the same for an action, you have to access the UIComponent from the actionEvent:

package ch.hasselba.xpages;
import java.util.List;
import javax.faces.event.ActionEvent;
import com.ibm.xsp.complex.Parameter;
import com.ibm.xsp.component.xp.XspEventHandler;

public class MyActionListener {
public void execute(ActionEvent event) {
        XspEventHandler eventHandler = (XspEventHandler) event.getComponent();
        List<Parameter> params = eventHandler.getParameters();
        for (Parameter p : params) {
            System.out.println(p.getName() + " -> " + p.getValue());
        }
     }
}

Here is an example XPage, the method execute is contained by the managed bean myAction:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

    <xp:button
        value="Label"
            id="buttonAction">
        <xp:eventHandler
            event="onclick"
            submit="true"
            immediate="true"
            refreshMode="complete"
            actionListener="#{myAction.execute}">
            <xp:this.parameters>
                <xp:parameter
                    name="param"
                    value="#{javascript:java.lang.System.currentTimeMillis()}">
                </xp:parameter>
            </xp:this.parameters>
        
        </xp:eventHandler>
            
    </xp:button>
    
</xp:view>

Quick-n-Dirty: Ajax Fileuploads

9. September 2013 Posted by Sven Hasselbach

Here is an example how to upload a file with Ajax to a XPage. It is a simple Javascript, and adds the required fields to the xhr request. This example works in FireFox and Chrome and should work on Domino >=8.5.2.

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

    <xp:this.data>
        <xp:dominoDocument
            var="document1"
            formName="RTItem"
            action="editDocument"
            concurrencyMode="force">
        </xp:dominoDocument>
    </xp:this.data>

    <xp:fileUpload onchange="handleFiles(this.files)"
        id="fileUpload1"
        value="#{document1.Body}">
    </xp:fileUpload>

    <xp:eventHandler event="onsaveDoc" submit="false" refreshMode="norefresh" immediate="false" save="true" id="saveDoc" />

    <xp:button
        value="ajaxSave"
        id="buttonAjax">
        <xp:eventHandler
            event="onclick"
            submit="false">
            <xp:this.script><![CDATA[sendFiles();]]></xp:this.script>
        </xp:eventHandler>
    </xp:button>

    <xp:div id="fileData" />

      <xp:scriptBlock id="scriptBlockAjax">
          <xp:this.value>
          <![CDATA[

          function handleFiles(files) {
            var i = 0;
            var dataDiv = dojo.byId('#{id:fileData}');
             var fileList = files;

            for(i = 0; i < fileList.length; i++)
            {
                    var img = document.createElement("img");
                    img.file = fileList[i];
                    img.name = 'no_'+ i;
                    img.classList.add("fileData");

                    var reader = new FileReader();
                    reader.onload = (function(aImg) { return function(e) { aImg.src = e.target.result; }; })(img);
                    reader.readAsDataURL(fileList[i]);

                    dataDiv.appendChild(img);    
            }
        }

        function FileUpload(img, file) {
             var xhr = new XMLHttpRequest();
            this.xhr = xhr;

            var frm = new FormData;
            frm.append( "#{id:fileUpload1}", file);
            frm.append( "$$viewid", dojo.byId('view:_id1__VUID').value);
            frm.append( "$$xspsubmitid", "#{id:saveDoc}");
            frm.append( "view:_id1", "view:_id1");

            xhr.open("POST", "#{javascript:facesContext.getExternalContext().getRequest().getRequestURI()}", true);
            xhr.send(frm);
        }

        function sendFiles(){
          var files = document.querySelectorAll(".fileData");

          for(var i = 0; i < files.length; i++)
          {
              new FileUpload(files[i], files[i].file);
          }

        }
        ]]>
        </xp:this.value>
    </xp:scriptBlock>

</xp:view>

The fileupload control executes the javascript method handleFiles when a new file is selected. This method “converts” the file to a DOM image and attaches it to the DIV fileData.

When clicking the button ajaxSend, the sendFiles method is fired and sends all selected files with xhr Requests to the server. The value of the $$submitid identifies a server side event which saves the document with the received attachment as new document.

Keep in mind: There is NO full refresh.

If you selecting some file in the browser…

.. and click on “ajaxSave“, the files are posted to the server:

The server side event saves the uploaded files to new documents:

Quick-n-Dirty: Upgrading XPiNC / XULRunner

17. Juli 2013 Posted by Sven Hasselbach

I have tested a small upgrade of XULRunner (the internal browser used for XPiNC applications) and successfully changed the version from 1.9.2.10 to 1.9.2.28 in DDE 8.5.3 and 9.0.

Version 1.9.2.10 was released on 15.10.2010
Release Notes for Firefox 3.6.10
Version 1.9.2.28 was released on 13.03.2012
Release Notes for Firefox 3.6.28

To do this by your own you have process the following steps:

  1. Close Notes and Designer completly
  2. Download the package for your platform from mozilla.org
  3. Extract the ZIP file
  4. Open the folder <NOTES>\framework\rcp\eclipse\plugins
  5. Open the folder com.ibm.rcp.xulrunner.runtime.win32.x86_6.2.3.20110915-1350 [The name of the folder depends on your DDE]
  6. Rename the folder xulrunner to xulrunner_bak (if you have problems with DDE you can switch back later by renaming the folder)
  7. Copy the xulrunner folder from the downloaded ZIP into this folder
  8. Restart Notes

P.S. Keep in mind that this article has been posted in the “Quick-n-Dirty” category.

XPages: Use a PhaseListener for global URL commands

11. Juli 2013 Posted by Sven Hasselbach

One of my customers wanted an easy way for printing XPages in different formats and with different content, depending of the current XPage opened in the browser. It was a requirement to develope a global solution for every XPage-based application in his company. That is why I created a global Java class which does not require to modify the existing applications at all. But it allows to customize and configure the output for the different needs of the departments and their required reports and print outs.

I decided to use a special PhaseListener to hook into the different applications. This allows the PDF generation depending on some URL parameters, and only runs if the URI contains a special command for printing the current XPage (but works for thw whole application).

The example code is what I have created:

package ch.hasselba.xpages.util;

import java.util.Map;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class PhaseListenerPDFGenerator implements PhaseListener {

    private static final long serialVersionUID = 1L;
    private static final String GENERATE_PDF = ".PDF";
    private static final String URL_PARAMETER_FILENAME = "filename";
    private static final String URL_PARAMETER_PDFTYPE = "type";
    private static final String HTTP_HEADER_CONTENTTYPE = "application/pdf";

    public void beforePhase(PhaseEvent event) {}

    public PhaseId getPhaseId() {
        return PhaseId.RESTORE_VIEW;
    }

    public void afterPhase(PhaseEvent event) {

        FacesContext facesContext = event.getFacesContext();

        HttpServletRequest req = (HttpServletRequest) facesContext
                .getExternalContext().getRequest();
        String uri = req.getRequestURI();

        if (uri.endsWith(GENERATE_PDF) == true ) {
            servePDF(facesContext);
        }
    }

    private void servePDF(FacesContext facesContext) {
        Map requestMap = facesContext.getExternalContext()
                .getRequestParameterMap();

        String pdfName = getPDFName(requestMap);
        String pdfType = getPDFType(requestMap);

        HttpServletResponse response = (HttpServletResponse) facesContext
                .getExternalContext().getResponse();

        try {

            response.setContentType( HTTP_HEADER_CONTENTTYPE );
            response.setHeader( "Content-Disposition", "attachment; filename="
                      + pdfName );
            response.setStatus(200);
            ServletOutputStream outputStream = response.getOutputStream();

            // Generate the PDF here
            // and send the data to the outputStream
            //
            //outputStream.write( PDFDATA );
            outputStream.flush();
            outputStream.close();
            facesContext.responseComplete();

        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

    public static String getPDFName(Map requestMap) {
        String pdfName = (String) requestMap.get( URL_PARAMETER_FILENAME );
        return pdfName;
    }
    public static String getPDFType(Map requestMap) {
        String pdfName = (String) requestMap.get( URL_PARAMETER_PDFTYPE );
        return pdfName;
    }

}

The PhaseListener scans the URI every time a request is sent to the server. As soon the URI ends with .PDF, the parameters fileName and type are extracted. The correct PDF template is identified and the requested PDF can be generated (Code for generating is not included in this example).

After generating the PDF, the output is written, and the response is completed. The file download headers are added to the response. The user receives a file download dialog and can open the generated PDF (and print it if required).

When this PhaseListener is enabled in the faces-config.xml, every XPage can use the URL command. For example if your XPage is named “MyXPage.xsp” the URL to enable the PDF generation instead the generation of the XPage looks like this:

http://example.com/path/to/db.nsf/MyXPage.xsp/.PDF?filename=test.pdf&type=exportAll

This returns a PDF with the filename test.pdf which is generated from the PDF template exportAll, containing the data of the current XPage. It is easy to add a link like this to your application to enable the PDF generation.

This is how the faces-config.xml has to look like:

<?xml version="1.0" encoding="UTF-8"?>
<faces-config>
  <lifecycle>
      <phase-listener>ch.hasselba.xpages.util.PhaseListenerPDFGenerator</phase-listener>
  </lifecycle>
</faces-config>

2 Security Bulletin IBM Notes 8.0.X, 8.5.X, 9.0

12. Juni 2013 Posted by Noteshexe

Abstract: In some scenarios, IBM Notes may fail to zero the plaintext password within memory, leaving the plaintext password accessible to an attacker with the ability to access memory on the user’s local workstation. Affected Plattform: Notes 9.0, 8.5.X Fix :  9.0 Interim Fix2, 8.5.3 FP4 Interim Fix 2, 8.5.3 FP5 This issue is being [...]

8.5.x Notes Client crashes related to Microsoft Internet Explorer 9

28. Mai 2013 Posted by Noteshexe

Problem: Random Notes client crashes no Notes 8.5.x after IE 9 upgrade Symptom: Client Crash Cause:  Customers who are using Notes 8.5.x on Windows 7 and have the Notes Preference ” Use the Browser that I have set as default for the Operating System”, can experience random client crashes. We have traces this issue back [...]

Quick-n-Dirty: Dojo 1.8 & Domino 8.5.3

25. April 2013 Posted by Sven Hasselbach

If you want to use Dojo 1.8 with Domino 8.5.3, you can do the following:

1. Grab the Dojo JAR file from a ND9 installation:

<PATH TO DOMINO>\osgi\shared\eclipse\plugins\com.ibm.xsp.dojo_9.0.0<VERSION>.jar

2. Create a folder “dojo-1.8.0” in your notes data directory

<PATH TO DOMINO DATA>\domino\js\dojo-1.8.0

3. Unpack the JAR file

4. Copy the content of the subfolder \resources\dojo-version of the unpacked JAR file in this folder:

5. Restart HTTP task.

6. Enjoy!

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
    <script>document.write(dojo.version)</script>
    <br/>
    <xp:label value="#{javascript:
        importPackage(com.ibm.xsp.core);
        Version.CurrentRuntimeVersion}" />
</xp:view>

P.S. Keep in mind that this article has been posted in the “Quick-n-Dirty” category.