XPages: Access resources and their content

To access XPages resources during runtime and to get their content as a string, you can use the following SSJS code snippet:

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

    <xp:label id="label1">
        <xp:this.value><![CDATA[#{javascript:
            var url = "/WEB-INF/faces-config.xml";
            var data = facesContext.getExternalContext().
                getResourceAsStream( url );
            var txt = "";
            while( data.available() ){
                txt += @Char(data.read());
            }
            txt}]]>
        </xp:this.value>
    </xp:label>

</xp:view>

[This displays the current faces-config.xml]

By using the correct URL path it is for example possible to access the code of SSJS libraries or even the source code of java classes in the database. To access the source code of a XPage, just use the name of the XPage itself. Same procedure for SSJS libraries, just add a „.jss“ at the end of the library’s name. For accessing a java class, the „dots“ in the package names have to be replaced with „slashes“.

To access f.e. the java file for class ch.hasselba.jsf.debug.ResourceUtil the url has to look like this:

var url = "ch/hasselba/jsf/debug/ResourceUtil.java";
Veröffentlicht unter Java, Java Script, ServerSide JavaScript, XPages | Verschlagwortet mit , , , , , | Ein Kommentar

XPages: Run your own Servlets

A really interesting article about running your own servlets on domino server can be found here: http://www.ibm.com/developerworks/cn/lotus/xpage-servlet/index.html

It’s chinese, but you can translate f.e. with Google’s Translator.

With 8.5.3 I had have some problems because the required interface IServletFactory could not be resolved in DDE.

To get the example running, you have to add a JAR to your build path. Open Project properties, select „Java Build Path“ and click „Add External JARs„:

Now you have to add the file „lwpd.domino.adapter.jar„. This can be found in the following folder:

<NOTES>\framework\shared\eclipse\plugins\com.ibm.domino.xsp.adapter_<VERSION>

The <VERSION> string depends on your current installation and Server-Version.

After adding this JAR to the build path, the compilation problems should be resolved.

Veröffentlicht unter Extensibility API, Java, JSF, Server, Web, XPages | Verschlagwortet mit , , , , , , | 6 Kommentare

XPages application events: Create your own ApplicationListener (2)

There is another interface available which provides the additional method applicationRefreshed. This event is always raised if the method refresh() of the Application-Object is fired.

Instead of implement the interface described in the previous posting, you have to use the interface named com.ibm.xsp.application.events.ApplicationListener2.

package ch.hasselba.jsf.debug;

import com.ibm.xsp.application.ApplicationEx;
import com.ibm.xsp.application.events.ApplicationListener2;

public class MyApplicationListener implements ApplicationListener2 {

    public void applicationCreated(ApplicationEx arg0) {
        System.out.println("applicationCreated()");
        System.out.println("ID: " + arg0.getApplicationId());
     }

    public void applicationDestroyed(ApplicationEx arg0) {
        System.out.println("applicationDestroyed()");
        System.out.println("ID: " + arg0.getApplicationId());
    }

    public void applicationRefreshed(ApplicationEx arg0) {
        System.out.println("applicationRefreshed()");
        System.out.println("ID: " + arg0.getApplicationId());
    }

}

You can fire the refresh of the application manually. Here is a simple example how to do this:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
   <xp:button value="Label" id="button1">
      <xp:eventHandler event="onclick" submit="true"
         refreshMode="complete">
            <xp:this.action>
               <![CDATA[#{javascript:
                  facesContext.getApplication().refresh()
               }]]>
            </xp:this.action>
        </xp:eventHandler>
   </xp:button>
</xp:view>

By clicking the button, the refresh is fired:

Veröffentlicht unter Extensibility API, Java, JSF, XPages | Verschlagwortet mit , , , , , , | 5 Kommentare

XPages application events: Create your own ApplicationListener

If you want to get a handle to application events and want to know if a XPages application is created or destroyed (which means the application was destroyed because of a time out), you can implement this by creating your own own application listener.

For this you have to do the following steps:

  1. Create a class which implements the ApplicationListener interface
  2. Activate the class in XPages runtime

To realise the first part, you have to create a class that implements the interface com.ibm.xsp.application.events.ApplicationListener. This interface has two methods: applicationCreated and applicationDestroyed (the method names should be self-describing enough).

Here is an example:

package ch.hasselba.jsf.debug;

import com.ibm.xsp.application.ApplicationEx;
import com.ibm.xsp.application.events.ApplicationListener;

public class MyApplicationListener implements ApplicationListener {

    public void applicationCreated(ApplicationEx arg0) {
        System.out.println("applicationCreated()");
        System.out.println("ID: " + arg0.getApplicationId());
    }

    public void applicationDestroyed(ApplicationEx arg0) {
        System.out.println("applicationDestroyed()");
        System.out.println("ID: " + arg0.getApplicationId());  
    }

}

Now to step two, the activation of the class. To do this, you have to add a special configuration file to your application:

  1. Switch to „Java“ perspective
  2. Create a new folder „META-INF“ in the „Code/Java“ section
  3. Create a sub folder „services
  4. Create a file named „com.ibm.xsp.core.events.ApplicationListener
  5. In this file you add the full name of your MyApplicationListener class (including the package name):

The file structure should now look like this in Java perspective:

If you switch back to Domino perspective, the structure in the „Code/Java“ looks like this:

You can now verify that all works correctly by opening a XPage and have a look on your server console:

[The application timeout was set to one minute.]

Veröffentlicht unter Extensibility API, Java, JSF, XPages | Verschlagwortet mit , , , , , , | Ein Kommentar

DocumentDataSource with Signer/SignerWithFullAccess-Rights

Yesterday I read the very interessting question from Daniele Grillo at stackoverflow.com: Is a datasource available for XPages who can access the underlying document with different access levels?

I have never seen one before, so I decided to do some tests and tried to find a workaround / hack for this problem. But after a while and a some lines of coding, I was unable to get a solution, so I decided to create a new datasource: A datasource which can access the documents in the backend with different access levels: sessionAsSigner and sessionAsSignerWithFullAccess.

The datasource is still in beta version, there are some limitations because I have implemented the basic functionallity only. It must be added programmatically, I have no ambitions to create a design element of it, but perhaps I will create a XSnippet.

To use this datasource, you can add it to a XPage and control it via URL parameters.

  • Open document as current user
XPage.xsp?documentId=<DocUNID>
  • Open document as Signer
XPage.xsp?documentId=<DocUNID>&access=sessionAsSigner
  • Open document as Signer with Full Access
XPage.xsp?documentId=<DocUNID>&access=sessionAsSignerWithFullAccess

To use this datasource in your XPage, you have to add some SSJS code:

<xp:this.beforePageLoad>
   <![CDATA[#{javascript:
      importPackage(ch.hasselba.xpages.jsf.core);
      var ds = new 
         ch.hasselba.xpages.jsf.core.AccessDocDataSource();
      ds.setVar( "document1" );
      ds.setConcurrencyMode( "force" );
      view.addData(ds);
   }]]>
</xp:this.beforePageLoad>

[You can change the name of the datasource (marked red) to fit your requirements.]

The datasource can be used like a normal document datasource:

<xp:inputText id="inputText1" value="#{document1.Test}" />

To save the document you have to call the save method directly (The default actions are currently not working. Maybe because the actions check the object type. I am trying to figure this out):

<xp:button value="Save" id="button1">
   <xp:eventHandler event="onclick" submit="true"
      refreshMode="complete">
      <xp:this.action>
         <![CDATA[#{javascript:document1.save()}]]>
      </xp:this.action>
   </xp:eventHandler>
</xp:button>

Here is the Java code:

package ch.hasselba.xpages.jsf.core;

import com.ibm.xsp.FacesExceptionEx;
import com.ibm.xsp.model.AbstractDocumentDataSource;
import com.ibm.xsp.model.DocDataSource;
import com.ibm.xsp.model.DocumentDataContainer;
import com.ibm.xsp.model.domino.wrapped.DominoDocument;
import javax.faces.context.FacesContext;
import javax.faces.el.ValueBinding;
import lotus.domino.Document;
import lotus.domino.NotesException;
import lotus.domino.Session;
import java.util.Map;

public class AccessDocDataSource extends AbstractDocumentDataSource
   implements DocDataSource {

    private final static String DEFAULT_CONCURRENCYMODE = "force";
    private final static String CONCURRENCYMODE = "concurrencyMode";
    private final static String ACTION = "action";
    private final static String ACCESS = "access";
    private final static String DOCUMENTID = "documentId";
    private final static String CONST_SESSION = "session";
    private final static String CONST_SESSIONASSIGNER = "sessionAsSigner";
    private final static String CONST_SESSIONASSIGNERFULLACCESS = "sessionAsSignerWithFullAccess";
    private final static String MSG_DOCSAVE_FAILED = "doSaveDocument failed!";

    private String _docId;
    private String _action;
    private String _concurrencyMode;
    private String _access;

    private DominoDocument _doc;

    private static boolean isStrEmpty(final String str) {
        if (str == null)
            return true;
        if (str.length() == 0)
            return true;

        return false;
    }

    private static Object getVariableValue(String varName) {

        FacesContext context = FacesContext.getCurrentInstance();
        return context.getApplication().getVariableResolver().resolveVariable(
                context, varName);
    }

    @Override
    public void readRequestParams(FacesContext paramFacesContext,
            Map<String, Object> pMap) {

        String tmpStr = (String) pMap.get(prefixRequestParam(DOCUMENTID));
        if (!isStrEmpty(tmpStr))
            this._docId = tmpStr;

        tmpStr = (String) pMap.get(prefixRequestParam(ACTION));
        if (!isStrEmpty(tmpStr))
            this._action = tmpStr;

        tmpStr = (String) pMap.get(prefixRequestParam(ACCESS));
        if (!isStrEmpty(tmpStr))
            this._access = tmpStr;
    }

    @Override
    public Object getDataObject() {

        return this._doc;
    }

    @Override
    public boolean isReadonly() {

        DominoDocument doc = getDocument();
        if (doc == null)
            return true;
        return isReadOnly( doc );

    }
    @Override
    public boolean isReadOnly(Object paramObject) {

        return !((DominoDocument) paramObject).isEditable();
    }
    @SuppressWarnings("deprecation")
    public DominoDocument getDocument() {

        Document d = null;
        DominoDocument doc = null;
        if (this._doc != null)
            return this._doc;

        try {
            Session s = (Session) getVariableValue(getAccess());

            d = s.getCurrentDatabase().getDocumentByUNID(getDocumentId());
            doc = com.ibm.xsp.model.domino.wrapped.DominoDocument.wrap(d
                    .getParentDatabase().getFilePath(), d, null,
                    getConcurrencyMode(), false, null);
            this._doc = doc;

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

        return doc;
    }

    public String getConcurrencyMode() {

        if (this._concurrencyMode != null) {
            return this._concurrencyMode;
        }

        ValueBinding vb = getValueBinding(CONCURRENCYMODE);
        if (vb != null) {
            return (String) vb.getValue(FacesContext.getCurrentInstance());
        }
        return DEFAULT_CONCURRENCYMODE;
    }

    public void setConcurrencyMode(String pString) {

        this._concurrencyMode = pString;
    }

    public String getDocumentId() {

        if (this._docId != null) {
            return this._docId;
        }

        ValueBinding vb = getValueBinding(DOCUMENTID);
        if (vb != null) {
            return (String) vb.getValue(FacesContext.getCurrentInstance());
        }

        return null;
    }

    public void setDocumentId(String pString) {

        this._docId = pString;
    }

    public String getAccess(){
        if (this._access != null) {
            return this._access;
        }

        ValueBinding vb = getValueBinding(ACCESS);
        if (vb != null) {
            return (String) vb.getValue(FacesContext.getCurrentInstance());
        }

        return null;
    }
    public void setAccess( String pString ){
        if( isStrEmpty( pString ) ){
            this._access = CONST_SESSION;
            return;
        }

        if( pString.equals( CONST_SESSIONASSIGNER ) ){
            this._access = CONST_SESSIONASSIGNER;
            return;
        }

        if( pString.equals( CONST_SESSIONASSIGNERFULLACCESS  ) ){
            this._access = CONST_SESSIONASSIGNERFULLACCESS ;
            return;
        }

        this._access = CONST_SESSION;

    }
    @Override
    public boolean doSaveDocument(FacesContext paramFacesContext, Object paramObject)
            throws FacesExceptionEx {

        try {
            return ((DominoDocument) paramObject).save();
        } catch (NotesException ne) {
            ne.printStackTrace();
        }
        throw new FacesExceptionEx( MSG_DOCSAVE_FAILED , null);
    }

    @Override
    public Object saveState(FacesContext paramFacesContext) {

        Object[] objArr = new Object[5];
        objArr[0] = super.saveState(paramFacesContext);
        objArr[1] = this._docId;
        objArr[2] = this._concurrencyMode;
        objArr[3] = this._action;
        objArr[4] = this._access;

        return objArr;
    }

    @Override
    public void restoreState(FacesContext paramFacesContext, Object paramObject) {

        Object[] objArr = (Object[]) paramObject;
        super.restoreState(paramFacesContext, objArr[0]);
        this._docId = ((String) objArr[1]);
        this._concurrencyMode = ((String) objArr[2]);
        this._action = ((String) objArr[3]);
        this._access = ((String) objArr[4]);
    }

    @Override
    public DocumentDataContainer doNewDocument(FacesContext paramFacesContext)
            throws FacesExceptionEx {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public DocumentDataContainer doOpenDocument(FacesContext paramFacesContext)
            throws FacesExceptionEx {
        // TODO Auto-generated method stub
        return null;
    }

    public boolean isNewDocument(FacesContext paramFacesContext) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public void doComputeDocument(FacesContext paramFacesContext, Object paramObject)
            throws FacesExceptionEx {
        // TODO Auto-generated method stub

    }

    @Override
    public void doDeleteDocument(FacesContext paramFacesContext, Object paramObject)
            throws FacesExceptionEx {
        // TODO Auto-generated method stub

    }

    @Override
    protected String composeUniqueId() {
        // TODO Auto-generated method stub
        return null;
    }

    public boolean isDocument(Object paramObject) {
        // TODO Auto-generated method stub
        return false;
    }

}
Veröffentlicht unter Java, JSF, Security, ServerSide JavaScript, XPages | Verschlagwortet mit , , , , , , | 7 Kommentare

Controlling the HTTP Expires Header

After reading a question on stack overflow about setting an own HTTP expires header and the problem that an additional header is generated automatically,  I made some tests how the domino server can be forced to stop this behaviour programmatically.

During my tests I was able to stop it by using  facesContext.responseComplete() but this works only for „headless“ XPages (set rendered to false). If you are calling the method in a normal XPage (set rendered to true), the generated output will be discarded and looks like the „headless“ version.

Here are two examples including screenshots from firebug console (containing the HTTP response):

  • Adding the header in beforeRenderResponse / beforePageLoad
<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
   <xp:this.beforeRenderResponse>
   <![CDATA[#{javascript:
      var ec = facesContext.getExternalContext();
      var response = ec.getResponse();
      var writer = response.getWriter();

      // set headers
      var now = new Date();
      response.setDateHeader("Expires",
         now.getTime() + (60*60*1000));
      response.setHeader("Cache-Control", "public");

      // Output it
      writer.write( now.getTime().toString() );
      }]]>
   </xp:this.beforeRenderResponse>
   <xp:label value="Test!" id="label1"></xp:label>
 </xp:view>

As you can see here, the output from the writer was added to the XPage first (the output will be added before the <HTML>-Tag and causes invalid HTML, but this can be ignored in this demo):

The response contains two HTTP expires header. The -1 is added after the programmatically generated one.

  • Adding the header in afterRenderResponse / afterPageLoad
<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
   <xp:this.afterRenderResponse>
   <![CDATA[#{javascript:
      var ec = facesContext.getExternalContext();
      var response = ec.getResponse();
      var writer = response.getWriter();

      // set headers
      var now = new Date();
      response.setDateHeader("Expires",
         now.getTime() + (60*60*1000));
      response.setHeader("Cache-Control", "public");

      // Output it
      writer.write( now.getTime().toString() );
      }]]>
   </xp:this.afterRenderResponse>
   <xp:label value="Test!" id="label1"></xp:label>
</xp:view>

In this scenario, the output is added after the HTML code.

The HTTP expires header was added first.

  • How to remove the header

After some research, I was able to remove the header programmatically:

First, you have to create a new java class which implements com.ibm.xsp.context.RequestParameters.ResponseCacheHeader.

package ch.hasselba.jsf.core;

import javax.servlet.http.HttpServletResponse;
import com.ibm.xsp.context.RequestParameters.ResponseCacheHeader;

public class SHResponseCacheHeader implements ResponseCacheHeader {

   public SHResponseCacheHeader(){}

   public boolean initResponseCacheHeader(HttpServletResponse arg0){
      return true;
   }
}

It is required that the initResponseCacheHeader() method returns true, otherwise this won’t work!

Then, you have to add the responseCacheHeader object to the requestParameters of facesContext:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core" rendered="false">
   <xp:this.beforePageLoad>
   <![CDATA[#{javascript:
      importPackage( ch.hasselba.jsf.core );

      var ec = facesContext.getExternalContext();
      var response = ec.getResponse();
      var writer = response.getWriter();

      // set headers
      var now = new Date();
      response.setDateHeader("Expires",
         now.getTime() + (60*60*1000));
      response.setHeader("Cache-Control", "public");

      // set response cache header object
      var reqParam = facesContext.getRequestParameters();
      var resCH = new SHResponseCacheHeader();
      reqParam.setResponseCacheHeader( resCH );

      // Output it
      writer.write( now.getTime().toString() );
      }]]>
   </xp:this.beforePageLoad>
   <xp:label value="Test!" id="label1"></xp:label>
 </xp:view>

The output will be generated as expected, but the useless HTTP expires header won’t be added anymore:

Veröffentlicht unter Java, JSF, Server, ServerSide JavaScript, Web, XPages | Verschlagwortet mit , , , , , , , , | Schreib einen Kommentar

XSnippets: Fire querySave / postSave – Events

The second XSnippet I have added to the XSnippet Contest is a help to fire the querySave– and postSave-events from SSJS: Save Datasource & Fire querySave/postSave events

If you only do a simple document1.save() , the events of a datasource won’t be executed. To fix this, you have to use the save() method of com.ibm.xsp.model.domino.DominoDocumentData instead:

var dsName = "document1.DATASOURCE"; // change this to the name of
                                     // the datasource you want
                                     // to save
var app = facesContext.getApplication();
var ds = app.getVariableResolver()
           .resolveVariable(facesContext, dsName);
ds.save( facesContext, true);

The variable dsName contains the name of the datasource to save, followed by „.DATASOURCE„. To use it f.e. with the current document, you have to change the variable to „currentDocument.DATASOURCE„.

The difference here is the type of object that is used: If you save a document datasource, the save method of an object of type NotesXspDocument is called. The com.ibm.xsp.model.domino.DominoDocumentData object has another method for saving. The first object type is like a backend NotesDocument, the second object is like the NotesUIDocument class.

Veröffentlicht unter Java Script, ServerSide JavaScript, Web, XPages, XSnippet | Verschlagwortet mit , , , , , | Schreib einen Kommentar

ObjectDataSource: Kleines „How To“

Mit der Extension Library bzw. dem Upgrade Pack 1 ist für XPages eine neue Datasource-Komponente hinzugekommen, die ObjectDataSource. Diese Datasource kann wie die Standard-Datasources für View und Dokument ebenfalls an den verschiedensten Elementen einer XPage angehangen werden, d.h. sowohl an die UIViewRoot der XPage, als auch in Custom Controls, Repeat Controls oder sonstigen Elementen, die eine DataSource verwenden.

Grundlegend gilt für ObjectDataSources, das sie beliebige Java-Objekte beinhalten können, solange die Objekte selbst serialisierbar sind, also das Interface java.io.Serializable implementieren. Hier ein einfaches Beispiel eines Java-Objektes:

package ch.hasselba.extlib.demo;

import java.io.Serializable;

public class ObjectDataDemo implements Serializable {

    long timeStamp;

    public ObjectDataDemo(){
        this.update();
    }

    public void update(){
        this.timeStamp = java.lang.System.currentTimeMillis();
    }

    public long getTimeStamp() {
        return timeStamp;
    }

}

Die Java-Klasse ist einfach aufgebaut und liefert den Zeitstempel zurück, der beim letzten Aufruf der update()-Methode gesetzt wurde. Um die Klasse in der XPage verwenden zu können, muss sie nur in einem Source-Folder bereitgestellt sein, der dem Build-Path hinzugefügt wurde bzw. die Klasse im neuen „JavaCode“-Ordner  abgelegt sein – eine Modifikation der faces-config.xml ist nicht nötig.

Mittels der createObject-Eigenschaft der ObjectDataSource wird die XPage dazu veranlasst, eine Instanz der Klasse anzulegen, womit das instanzierte Objekte der ObjectDataSource gehört; um die Stabilität einer Applikation nicht zu gefährden und unerwartete Ergebnisse zu provozieren, sollten die Objekte daher nicht anderweitig gespeichert werden (z.B. in Managed Beans o.ä.).

<xp:this.data>
   <xe:objectData var="objectData1" scope="view"
      createObject="#{javascript:
         new ch.hasselba.extlib.demo.ObjectDataDemo();}">
   </xe:objectData>
</xp:this.data>

Eine ObjectDataSource hat neben dem zu verwendenden scope auch noch die Eigenschaft var, die (wie bei den anderen Datasources auch) den programmatischen Namen darstellt, mit dem die DataSource referenziert werden kann. In diesem Fall ist das Objekt und alle Eigenschaften/Methoden des Objektes über objectData1 zu erreichen:

<xp:label id="label1"
   value="#{javascript:objectData1.getTimeStamp()}" />

Da z.B. ein Partial Refresh die Datasources nicht jedesmal neuberechnet, muss ein eventuell benötigter Update-Mechanismus selbst implementiert werden. In diesem Beispiel dient dafür die Methode update(), die im Backend aufgerufen werden kann:

<xp:button value="Label" id="button1">
   <xp:eventHandler event="onclick" submit="true"
      refreshMode="partial" refreshId="label1">
      <xp:this.action>
         <![CDATA[#{javascript:objectData1.update()}]]>
      </xp:this.action>
   </xp:eventHandler>
</xp:button>

[Button ruft die Update-Funktion auf, label1 wird refresht und neuer Zeitstempel angezeigt]

Die ObjectDataSources besitzen eine weitere Eigenschaft namens saveObject. Diese Eigenschaft ist optional und nur erforderlich, wenn man beabsichtigt, auf ein Speichern der DataSources reagieren zu wollen (um z.B. Daten ins Backend zu speichern, o.ä.).

Achtung:

Einige Methodennamen dürfen in der Java-Klasse nicht verwendet werden, da dies sonst zu Problemen mit der bestehenden Architektur kommen kann. Namen wie save() oder load() sind zu vermeiden!

Veröffentlicht unter ExtLib, Java, JSF, ServerSide JavaScript, XPages | Verschlagwortet mit , , , , , , , | Schreib einen Kommentar

Bug: facesContext.getRenderResponse()

Eigentlich sollte die Methode  getRenderResponse() true zurück liefern, wenn der Code in der Render Response-Phase ausgeführt wird, doch leider ist die Funktion nicht korrekt implementiert. So liefert die Funktion bei einem normalen Seitenaufruf falsche Ergebnisse, bei einem Partial Refresh hingegen funktioniert sie jedoch wie sie soll.

Zur Erläuterung verwende ich folgende XPage:

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

   <xp:label value="Label" id="label1">
      <xp:this.rendered>
         <![CDATA[#{javascript:
            if( facesContext.getRenderResponse() ){
               print( "Render Response!" );
            }else{
               print( "NOT Render Response!" );
            }
            return true}]]>
      </xp:this.rendered>
   </xp:label>

   <xp:button value="Label" id="button1">
      <xp:eventHandler event="onclick" submit="true"
           refreshMode="partial" refreshId="label1">
      </xp:eventHandler>
   </xp:button>
   
</xp:view>

Öffnet man die XPage im Browser, wird auf der Serverkonsole folgendes ausgegeben:

Bei einem Partial Refresh hingegen wird die Phase korrekt erkannt:

Veröffentlicht unter Bug, JSF, ServerSide JavaScript, XPages | Verschlagwortet mit , , , , , , , , | Schreib einen Kommentar

Performance-Killer in der XPage

Dank einer Frage von Ulrich Krause im XPages Developer Forum wurde ein Thema „wiederbelebt“, das mir vor einiger Zeit in einem Projekt aufgefallen ist und sich als wahre Bremse bei XPages-Applikationen herausstellt:

Sämtliche Datenquellen (DataContext-Variablen, Repeat Controls, usw.) werden bei jedem Partial Refresh neu berechnet, auch wenn sie nicht das Ziel (bzw. ein Kind-Element des Ziels) des Refreshs sind.

Ich nutze als Beispiel hierfür die von Ulrich Krause gepostete Beispiel-XPage, um das Problem zu verdeutlichen. Den fett markierten Code ist von mir zur Verdeutlichung auf der Serverkonsole eingebaut worden, dass es sich nicht um einen fehlerhaftes print-Statement handelt, sondern der Code wirklich neu berechnet wird.

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
 <xp:button value="Label" id="button1">
 <xp:eventHandler event="onclick" submit="true"
 refreshMode="partial" refreshId="col1" />
 </xp:button>
 <xp:table>
 <xp:tr>
 <xp:td id="col1">
 <xp:text escape="true" id="computedField2"
   value="#{javascript:@Now()}">
 <xp:this.converter>
 <xp:convertDateTime type="both" />
 </xp:this.converter>
 </xp:text>
 </xp:td>
 <xp:td id="col2">
 <xp:text escape="true" id="computedField1"
   value="#{javascript:@Now()}">
 <xp:this.converter>
 <xp:convertDateTime type="both" />
 </xp:this.converter>
 </xp:text>
 <xp:dataTable id="dataTable1" rows="30">
 <xp:this.value><![CDATA[#{javascript:
 print(java.lang.System.currentTimeMillis() + ' You shall not refresh');
}]]></xp:this.value>
 <xp:column id="column1" />
 </xp:dataTable>
 </xp:td>
 </xp:tr>
 </xp:table>
</xp:view>

Wird der Button geklickt, wird ein Refresh auf das Element mit der Id „col1“ ausgelöst, und eigentlich dürfte die Data Table nicht neu berechnet werden, doch genau das ist aber der Fall!

[Die Ausgabe auf der Serverkonsole habe ich mit einem Debug PhaseListener kombiniert, um die Phasen im Lifecycle hervorzuheben]

Button

<xp:button value="Label" id="button1">
   <xp:eventHandler event="onclick" submit="true"
      refreshMode="partial" refreshId="col1" />
</xp:button>

1. Öffnen der XPage

19.02.2012 18:02:17   HTTP JVM: Before phase: RENDER_RESPONSE 6
19.02.2012 18:02:17   HTTP JVM: 1329670937472 You shall not refresh
19.02.2012 18:02:17   HTTP JVM: After phase: RENDER_RESPONSE 6

2. Partial Refresh

19.02.2012 18:05:37   HTTP JVM: Before phase: RESTORE_VIEW 1
19.02.2012 18:05:37   HTTP JVM: After phase: RESTORE_VIEW 1
19.02.2012 18:05:37   HTTP JVM: Before phase: APPLY_REQUEST_VALUES 2
19.02.2012 18:05:37   HTTP JVM: 1329671137733 You shall not refresh
19.02.2012 18:05:37   HTTP JVM: After phase: APPLY_REQUEST_VALUES 2
19.02.2012 18:05:37   HTTP JVM: Before phase: PROCESS_VALIDATIONS 3
19.02.2012 18:05:37   HTTP JVM: After phase: PROCESS_VALIDATIONS 3
19.02.2012 18:05:37   HTTP JVM: Before phase: UPDATE_MODEL_VALUES 4
19.02.2012 18:05:37   HTTP JVM: After phase: UPDATE_MODEL_VALUES 4
19.02.2012 18:05:37   HTTP JVM: Before phase: INVOKE_APPLICATION 5
19.02.2012 18:05:37   HTTP JVM: After phase: INVOKE_APPLICATION 5
19.02.2012 18:05:37   HTTP JVM: Before phase: RENDER_RESPONSE 6
19.02.2012 18:05:37   HTTP JVM: 1329671137769 You shall not refresh
19.02.2012 18:05:37   HTTP JVM: After phase: RENDER_RESPONSE 6

Die Berechung der DataTable erfolgt zwei Mal währen des Partial Refreshs.

Ändert man die Ausführung des Buttons auf Partial Excecution, erhält man folgendes Ergebnis:

Button

<xp:button value="Label" id="button1" execMode="partial">
   <xp:eventHandler event="onclick" submit="true"
   refreshMode="partial" refreshId="col1" />
</xp:button>

1. Öffnen der XPage

19.02.2012 18:13:54   HTTP JVM: Before phase: RENDER_RESPONSE 6
19.02.2012 18:13:54   HTTP JVM: 1329671634490 You shall not refresh
19.02.2012 18:13:54   HTTP JVM: After phase: RENDER_RESPONSE 6

2. Partial Refresh

19.02.2012 18:14:02   HTTP JVM: Before phase: RESTORE_VIEW 1
19.02.2012 18:14:02   HTTP JVM: After phase: RESTORE_VIEW 1
19.02.2012 18:14:02   HTTP JVM: Before phase: APPLY_REQUEST_VALUES 2
19.02.2012 18:14:02   HTTP JVM: 1329671642321 You shall not refresh
19.02.2012 18:14:02   HTTP JVM: After phase: APPLY_REQUEST_VALUES 2
19.02.2012 18:14:02   HTTP JVM: Before phase: PROCESS_VALIDATIONS 3
19.02.2012 18:14:02   HTTP JVM: After phase: PROCESS_VALIDATIONS 3
19.02.2012 18:14:02   HTTP JVM: Before phase: UPDATE_MODEL_VALUES 4
19.02.2012 18:14:02   HTTP JVM: After phase: UPDATE_MODEL_VALUES 4
19.02.2012 18:14:02   HTTP JVM: Before phase: INVOKE_APPLICATION 5
19.02.2012 18:14:02   HTTP JVM: After phase: INVOKE_APPLICATION 5
19.02.2012 18:14:02   HTTP JVM: Before phase: RENDER_RESPONSE 6
19.02.2012 18:14:02   HTTP JVM: 1329671642360 You shall not refresh
19.02.2012 18:14:02   HTTP JVM: After phase: RENDER_RESPONSE 6

Wieder erfolgt die Berechung der DataTable zwei Mal währen des Partial Refreshs.

Eine Änderung des Execution-Modes des Events verringert die Berechnung zumindest auf eine Neuberechnung pro Partial Refresh:

Button

<xp:button value="Label" id="button1" execMode="partial">
   <xp:eventHandler event="onclick" submit="true"
   refreshMode="partial" refreshId="col1" execMode="partial"/>
</xp:button>

1. Öffnen der XPage

19.02.2012 18:23:17   HTTP JVM: Before phase: RENDER_RESPONSE 6
19.02.2012 18:23:17   HTTP JVM: 1329672197161 You shall not refresh
19.02.2012 18:23:17   HTTP JVM: After phase: RENDER_RESPONSE 6

2. Partial Refresh

19.02.2012 18:23:18   HTTP JVM: Before phase: RESTORE_VIEW 1
19.02.2012 18:23:18   HTTP JVM: After phase: RESTORE_VIEW 1
19.02.2012 18:23:18   HTTP JVM: Before phase: APPLY_REQUEST_VALUES 2
19.02.2012 18:23:18   HTTP JVM: After phase: APPLY_REQUEST_VALUES 2
19.02.2012 18:23:18   HTTP JVM: Before phase: PROCESS_VALIDATIONS 3
19.02.2012 18:23:18   HTTP JVM: After phase: PROCESS_VALIDATIONS 3
19.02.2012 18:23:18   HTTP JVM: Before phase: UPDATE_MODEL_VALUES 4
19.02.2012 18:23:18   HTTP JVM: After phase: UPDATE_MODEL_VALUES 4
19.02.2012 18:23:18   HTTP JVM: Before phase: INVOKE_APPLICATION 5
19.02.2012 18:23:18   HTTP JVM: After phase: INVOKE_APPLICATION 5
19.02.2012 18:23:18   HTTP JVM: Before phase: RENDER_RESPONSE 6
19.02.2012 18:23:18   HTTP JVM: 1329672198875 You shall not refresh
19.02.2012 18:23:18   HTTP JVM: After phase: RENDER_RESPONSE 6

Selbst das Setzen der execId auf col1 ändert nichts – der Refresh führt immer zu einer Neuberechnung der DataTable.

Da diese Neuberechnung bei jedem Partial Refresh ausgelöst wird, werden Applikationen extrem ausgebremst. Daher sollte die Berechnung der Datenquelle wenn möglich auf „Computed On Page Load“ gesetzt werden.

Wenn dies nicht möglich ist, habe ich hier ein kleines Snippet, das die Berechnung nur dann durchführt, wenn die Datenquelle auch wirklich refresht wird:

<xp:repeat id="repeat1" rows="30" var="rCol">
   <xp:this.value><![CDATA[#{javascript:
      function calculate(){
         var data:java.util.Vector = new java.util.Vector();
         data.add(java.lang.System.currentTimeMillis());
         return data;
      }

      if( viewScope.containsKey("data") == false){
         viewScope.data = calculate();
         return viewScope.data;
      }
      if( com.ibm.xsp.ajax.AjaxUtil.isAjaxPartialRefresh(facesContext) === true ){
         var pId = com.ibm.xsp.ajax.AjaxUtil.getAjaxComponentId( facesContext  );
         if( pId !== getClientId( 'repeat1' ) )
            return viewScope.data;
      }

      viewScope.data = calculate();
      viewScope.data}]]>
   </xp:this.value>
   <xp:label value="#{javascript:rCol }" id="label1"></xp:label>
</xp:repeat>

[Hier ein Beispiel mit einem Repeat Control]

Die Version ist recht simpel aufgebaut, eine verbesserte Version werde ich in den nächsten Tagen als XSnippet veröffentlichen.

Veröffentlicht unter Expression Language, JSF, Performance, ServerSide JavaScript, XPages | Verschlagwortet mit , , , , , , , , | 3 Kommentare