Posts Tagged: ‘Designer’

XPages: WebContent Files (2) – Manipulate exitsting files using the Java NAPI

19. November 2014 Posted by Sven Hasselbach

In this article, I will shortly give an overview how you can edit existing file from the WebContent folder (Don’t miss the first article on this topic).

First, let’s create a view to display the design elements of the WebContent folder. To do this, I have an old school LotusScript Agent which updates the selection formula of a view (Some details about this technique can be found here).

Sub Initialize

    Dim session As New NotesSession
    Dim doc As NotesDocument
    Dim db As NotesDatabase
    Dim view As NotesView
      
    Set db = session.Currentdatabase
    Set view = db.Getview("DesignView")
    
    view.SelectionFormula = |@Contains($FlagsExt; "w")|
    
    Set doc = db.GetDocumentByUNID(view.UniversalID)
    Delete view
    
    doc.ReplaceItemValue "$FormulaClass", "7FFF"
    doc.Sign
    doc.Save True, False

End Sub

The agent has to run once to change the view’s selection criteria. In this example the view has the name “DesignView”. After that, we can add a single column to the view to display the files and their names:

Now lets build a simple XPage named Files.xsp to select the file you want to edit:

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

    <xp:viewPanel
        rows="30"
        id="viewPanel1"
        var="rowEntry">
        <xp:this.facets>
            <xp:pager
                partialRefresh="true"
                layout="Previous Group Next"
                xp:key="headerPager"
                id="pager1">
            </xp:pager>
        </xp:this.facets>
        <xp:this.data>
            <xp:dominoView
                var="viewDesign"
                viewName="DesignView">
            </xp:dominoView>
        </xp:this.data>
        <xp:viewColumn
            columnName="$FileNames"
            id="viewColumnFileNames"
            displayAs="link"
            >
            <xp:this.pageUrl>
                <![CDATA[#{javascript:"/Editor.xsp?filename=" +
                   rowEntry.getColumnValues().get(0)}]]>
        </xp:this.pageUrl>
        </xp:viewColumn>
    </xp:viewPanel>
    <xp:br />

</xp:view>

The Files.xsp lists all files as links and opens them via the Editor.xsp XPage:

This is the Editor.xsp:

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

    FileName:
    <xp:inputText
        id="inputTextFileName"
        value="#{fileBean.fileName}"
        defaultValue="#{param.filename}"
        disabled="true" />
    <xp:br />
    FileData:

    <xp:inputTextarea
        id="inputTextarea1"
        value="#{fileBean.fileData}"
        rows="40"
        cols="80" />
    <xp:br />
    <xp:button
        value="Load"
        id="buttonLoad">
        <xp:eventHandler
            event="onclick"
            submit="true"
            refreshMode="complete"
            action="#{fileBean.loadData}">
        </xp:eventHandler>
    </xp:button>
    <xp:button
        value="Save"
        id="buttonSave">
        <xp:eventHandler
            event="onclick"
            submit="true"
            refreshMode="complete"
            action="#{fileBean.saveData}">
        </xp:eventHandler>
    </xp:button>
    
</xp:view>

It uses a simple managed bean…

package ch.hasselba.napi;

import java.io.Serializable;

public class FileDataBean implements Serializable {

    private static final long serialVersionUID = 1L;
    private String fileName;
    private String fileData;
    private String dbPath;
    private String dbServer;

    public String getDbPath() {
        return dbPath;
    }

    public void setDbPath(String dbPath) {
        this.dbPath = dbPath;
    }

    public String getDbServer() {
        return dbServer;
    }

    public void setDbServer(String dbServer) {
        this.dbServer = dbServer;
    }

    public void setFileData(String fileData) {
        this.fileData = fileData;
    }

    public String getFileData() {
        return fileData;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public String getFileName() {
        return fileName;
    }

    public void loadData() {
        this.fileData = NAPIUtils.loadFile(this.dbServer, this.dbPath, this.fileName);
    }

    public void saveData() {
        NAPIUtils.saveFile(this.dbServer, this.dbPath, this.fileName, this.fileData);
    }
}

… which is initialized with the properties defined in the faces-config.xml. There you can find the database server and the database path:

<?xml version="1.0" encoding="UTF-8"?>
<faces-config>
  <managed-bean>
    <managed-bean-name>fileBean</managed-bean-name>
    <managed-bean-class>ch.hasselba.napi.FileDataBean</managed-bean-class>
    <managed-bean-scope>view</managed-bean-scope>
    <managed-property>
      <property-name>dbPath</property-name>
      <value>NAPI.nsf</value>
    </managed-property>
    <managed-property>
      <property-name>dbServer</property-name>
      <value>DEV01/Hasselba/CH</value>
    </managed-property>
  </managed-bean>
</faces-config>

And last but not least, the required NAPIUtil class:

package ch.hasselba.napi;

import java.io.InputStream;
import com.ibm.designer.domino.napi.NotesAPIException;
import com.ibm.designer.domino.napi.NotesDatabase;
import com.ibm.designer.domino.napi.NotesNote;
import com.ibm.designer.domino.napi.NotesObject;
import com.ibm.designer.domino.napi.NotesSession;
import com.ibm.designer.domino.napi.design.FileAccess;

public class NAPIUtils {

    /**
     * loads a given WebContent file and returns the result as String
     * 
     * @param serverName
     *            the server to use
     * @param dbPath
     *            the database path
     * @param fileName
     *            the file to load
     * @return the file data as String
     */
    static public String loadFile(final String serverName, final String dbPath,
            final String fileName) {

        NotesSession nSession = null;
        NotesDatabase nDatabase = null;
        NotesNote nNote = null;

        try {
            nSession = new NotesSession();

            // open database
            nDatabase = nSession.getDatabaseByPath(serverName + "!!" + dbPath);
            nDatabase.open();

            // load existing data
            nNote = FileAccess.getFileByPath(nDatabase, fileName);

            // get Filedate and return String
            InputStream is = FileAccess.readFileContentAsInputStream(nNote);

            return convertStreamToString(is);
        } catch (NotesAPIException e) {
            e.printStackTrace();
        } finally {
            // recycle NAPI objects
            recycleNAPIObject(nNote, nDatabase, nSession);
        }

        return fileName;
    }

    /**
     * loads a given WebContent file and returns the result as String
     * 
     * @param serverName
     *            the server to use
     * @param dbPath
     *            the database path
     * @param fileName
     *            the file to load
     * @param fileData
     *            the data of the file
     */
    static public void saveFile(final String serverName, final String dbPath,
            final String fileName, final String fileData) {

        NotesSession nSession = null;
        NotesDatabase nDatabase = null;
        NotesNote nNote = null;

        try {
            nSession = new NotesSession();

            // open database
            nDatabase = nSession.getDatabaseByPath(serverName + "!!" + dbPath);
            nDatabase.open();

            // load existing data
            nNote = FileAccess.getFileByPath(nDatabase, fileName);

            // store them to note
            FileAccess.saveData(nNote, fileName, fileData.getBytes());

        } catch (NotesAPIException e) {
            e.printStackTrace();
        } finally {
            // recycle NAPI objects
            recycleNAPIObject(nNote, nDatabase, nSession);
        }
    }

    /**
     * converts an input stream to a string
     * 
     * @param is
     *            the input stream to convert
     * @return String
     */
    static String convertStreamToString(java.io.InputStream is) {
        java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\A");
        return s.hasNext() ? s.next() : "";
    }

    /**
     * recycleNAPIObject helper method for recycling NAPI objects
     * 
     * @param nObjects
     *            the NAPI objects to recycle
     */
    static void recycleNAPIObject(NotesObject... nObjects) {
        for (NotesObject nObject : nObjects) {
            if (nObject != null) {
                try {
                    (nObject).recycle();
                } catch (NotesAPIException ne) {
                }
            }
        }
    }
}

If the class looks like this…

…just hover one  of the entries and select “Fix project setup”.

Then, you can choose the missed bundle:

Build the project, and open one of the files by clicking a link in the Files.xsp. Here is an example for the file WEB-INF/faces-config.xml:

Now you can click the “Load” button to read the content of the file.

You can edit the file now and save it to the NSF.

If you got to package explorer (Hit F9 for refresh) you can see the changes:

XPages: WebContent Files (1) – Create a file using the Java NAPI

18. November 2014 Posted by Sven Hasselbach

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

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: Create your own Required Validators

20. Juni 2013 Posted by Sven Hasselbach

If you try to implement your own JSF Validator (by implementing javax.faces.validator.Validator), you will notice that you are unable to check for an empty value. The reason for this is rather simple: The method validate() is only called, if there is something to do. If the value is empty, there is nothing to do, that’s why nothing happens.

But wait! What about the available required validator for XPages?

<xp:inputText id="inputText1">
   <xp:this.validators>
      <xp:validateRequired></xp:validateRequired>
   </xp:this.validators>
</xp:inputText>

And yes, this is a validator, but the IBM implemented a workaround for their UIComponents: If the UIComponents required property is set to true, the UIComponent knows that she needs to be filled in. Or a validator has to be attached to the UIComponent which implements the com.ibm.xsp.validator.FacesRequiredValidator interface.

Here is an example:

package ch.hasselba.xpages.core;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.ValidatorException;
import com.ibm.xsp.validator.FacesRequiredValidator;

public class RequiredValidator implements FacesRequiredValidator {

    public String getRequiredMessage() {
        return "Error! Required field is empty!";
    }

    public void validate(FacesContext facesContext, UIComponent uiComponent,
            Object obj) throws ValidatorException {

    }

}

When processing the UIComponent data, the attached validators are checked if they are an instance of FacesRequiredValidator, and this is the reason why you cannot attach this validator to a component in the DDE directly: Because if you define the validator in the faces-config.xml and add it to the UIComponent with the validator id…

<xp:inputText id="inputText1">
   <xp:this.validators>
      <xp:validator validatorId="myValidator" />
   </xp:this.validators>
</xp:inputText>

… the XPages engine attaches a validator which is not implementing the FacesRequiredValidator interface. Instead, an instance of com.ibm.xsp.validator.ValidatorImpl is attached.

You have to do this by your own, for example in the beforeRenderResponse event:


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

   <xp:this.beforeRenderResponse>
      <![CDATA[#{javascript:getComponent('inputText1').addValidator(
         new ch.hasselba.xpages.core.RequiredValidator()
      );}]]>
   </xp:this.beforeRenderResponse>

   <xp:button
      value="Label"
      id="button1">
      <xp:eventHandler event="onclick" submit="true"
         refreshMode="complete" immediate="false" save="true" />
   </xp:button>

   <xp:br />

   <xp:messages id="messages1"></xp:messages>

   <xp:br />

   <xp:inputText id="inputText1" />

</xp:view>

[This example is just for a demonstration. It contains a bug, the validator will attached over and over again, so you have to check if the validator is already attached or not]

If you now open the XPage, your validator will work as expected:

 

Quick-n-Dirty: How to add HTML 5 events

26. März 2013 Posted by Sven Hasselbach

One way for adding unsupported events to an XPage or a component is the trick from Keith. But this is limited to CSJS only. If you need to execute a server side event, you just need change the name of the event to a new HTML 5 event name which does not exist in the DDE.

Here is an example for the new onSearch event:

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

    <xp:inputText id="inputTextSearch" type="search">
        <xp:eventHandler event="onsearch" submit="true"
            refreshMode="partial" refreshId="labelSearchFor">
            <xp:this.action>
               <![CDATA[#{javascript:print("onSearch");}]]>
            </xp:this.action>
        </xp:eventHandler>
    </xp:inputText>

    <xp:br />
    <xp:br />

    <xp:label id="labelSearchFor">
        <xp:this.value>
           <![CDATA[#{javascript:getComponent("inputTextSearch").value}]]>
        </xp:this.value>
    </xp:label>

</xp:view>

After entering a value and hitting enter…

… you can see on the server console that the event occurred:

This technique allows to add new events to the whole XPage, for example the onContextMenu event:

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

        <xp:eventHandler event="oncontextmenu" submit="true"
            refreshMode="norefresh">
            <xp:this.action>
                <![CDATA[#{javascript:print("onContextMenu");}]]>
            </xp:this.action>
        </xp:eventHandler>

</xp:view>

Every time if the user opens the context menu in the browser, the event is triggered and executed on the server:

The name of the event must be lowercased. A list of HTML 5 events can be found here: http://www.tutorialspoint.com/html5/html5_events.htm. Not all browsers support every HTML 5 event in the list.

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

Quick-n-Dirty: Import SSJS libraries with DXL

10. Januar 2013 Posted by Sven Hasselbach

In the last time I have developed different techniques for manipulating the design elements of XPages applications. While I am still working on a way for manipulating the localization files, I was playing a little bit with DXL imports.

Here comes a Java class to import a SSJS library with DXL to a database. It allows to create a new library directly from the browser:

By clicking the Import button the new Library will be added to your database (perhaps you have to refresh the DDE):

To use the new imported code you have to sign the library first (or the complete database), otherwise you will receive a security error (This can be done in the XPage too, just alter the code of the button).

Here is the code of the example XPage:

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

    <xp:inputTextarea id="inputTextareaSSJSCode"
       value="#{sessionScope.SSJSCode}">
    </xp:inputTextarea>

    <xp:br />

    <xp:inputText id="inputTextSSJSLib" value="#{sessionScope.SSJSLib}" />

    <xp:br />

    <xp:button value="Import" id="buttonImport">
       <xp:eventHandler event="onclick" submit="true" refreshMode="complete">
       <xp:this.action>
          <![CDATA[#{javascript:
             importPackage( ch.hasselba.xpages.util );

             var dxl = SSJSLibImporter.convertToDXL(
             sessionScope.SSJSCode, sessionScope.SSJSLib,
                database.getReplicaID()
             );

             var dxlImp = sessionAsSignerWithFullAccess.createDxlImporter();
             dxlImp.setDesignImportOption( 6 );
             dxlImp.importDxl( dxl,
                sessionAsSignerWithFullAccess.getCurrentDatabase() );
          }]]>
       </xp:this.action>
     </xp:eventHandler>
   </xp:button>

</xp:view>

And here is the Java class:

package ch.hasselba.xpages.util;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import com.ibm.misc.BASE64Encoder;

/**
 * Helper Class for DXL Import
 * of a SSJS library
 *
 * @author Sven Hasselbach
 * @version 0.2
 * @category DXL
 * @category Util
 */
public class SSJSLibImporter {

    /**
     * converts a integer to a byte array
     * in little endian order
     *
     * @param i integer to convert
     * @return byte array
     */
    static byte[] toBytes(final int i)
    {
            ByteBuffer b = ByteBuffer.allocate(4);
            b.order( ByteOrder.LITTLE_ENDIAN );
            b.putInt(i);
                return b.array();
    }

    /**
     * encapsulates raw SSJS lib data to RT records
     *  
     * @param data array to encapsulate
     * @return byte array with RT records
     */
    public static byte[] generateSSJSBlock( byte[] data ){

            final int RECORDLENGTH_OVERALL = 50;
            final int RECORDLENGTH_BLOB = 18;

            int size = data.length;  // size of the data
            int roundedSize = (size % 2 == 0)?size:size+1; // rounded size

            byte[] record = new byte[ roundedSize + RECORDLENGTH_OVERALL ];
            byte[] hlp;

            // Record Type (Event)
            record[0] = -7;
            record[1] = -1;

            // Record Length
            record[2] = 32;
            record[3] = 0;

            // event type
            record[8] = 22;
            record[9] = 0;

            // action type
            record[10] = 4;
            record[11] = 0;

            // data size
            hlp = toBytes( size );
            record[12] = hlp[0];
            record[13] = hlp[1];

            // Record Type (Blob)
            record[32] = -36;
            record[33] = -1;

            // Rounded size + recordlength
            hlp = toBytes( roundedSize + RECORDLENGTH_BLOB );
            record[34] = hlp[0];
            record[35] = hlp[1];

            record[36] = -7;
            record[37] = -1;

            // Rounded size
            hlp = toBytes( roundedSize );
            record[38] = hlp[0];
            record[39] = hlp[1];

            record[40] = 32;
            record[41] = 78;

            // Add data to BLOB          
            for( int  i=0; i<data.length; i++ ){
                    record[i+RECORDLENGTH_OVERALL] = data[i];
            }
            return record;
    }

    /**
     * generates the DXL to import a new SSJS library
     *
     * @param data array of bytes 
     * @param libName Name of the SSJS library
     * @param dbReplicaId
     * @return DXL to import
     * 
     */
    public static String generateSSJSDXL( byte[] data, final String libName, final String dbReplicaId ){
            StringBuffer dxl = new StringBuffer();
            BASE64Encoder benc = new BASE64Encoder();
            dxl.append( "<?xml version='1.0'?><!DOCTYPE scriptlibrary SYSTEM 'xmlschemas/domino_8_5_3.dtd'>" );
            dxl.append( "<scriptlibrary name='" + libName + "' xmlns='http://www.lotus.com/dxl' version='8.5' " );
            dxl.append( "maintenanceversion='3.0' replicaid='" + dbReplicaId + "' hide='v3 v4strict' " );
            dxl.append( "designerversion='8.5.3'>" );
            dxl.append( "<item name='$Flags'><text>.5834Q</text></item>" );
            dxl.append( "<item name='$ServerJavaScriptLibrary' sign='true'>" );
            dxl.append( "<rawitemdata type='1'>" );
            dxl.append( benc.encode( data ) );
            dxl.append( "</rawitemdata></item></scriptlibrary>" );

            return dxl.toString();

    }

    /**
     * converts a String to a DXL for a SSJS library to import
     *
     * @param String to convert
     * @param Library name
     * @param ReplicaId
     * @return String with DXL
     */
    public static String convertToDXL( String raw, String libName, String dbReplicaId ){
            byte[] data = generateSSJSBlock( raw.getBytes() );
            return generateSSJSDXL( data, libName, dbReplicaId );          
    }

}

This class only imports a new SSJS library. If you want to update an existing library, you need to add a <note-info> tag to the DXL. And don’t forget to update the ACL of your database, otherwise the DXL import will fail!

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

XPages: UI for editing localization files (1)

17. Dezember 2012 Posted by Sven Hasselbach

For one of my customers I am developing an easy way for editing the localization/translation of existing XPages applications: Instead of importing and exporting the property files with the Domino Designer (which is not an option for non-developers) it would be more suitable, if the users would have the possibility to edit the files directly in the browser. I am currently  working on a XPage solution for this, and this is my current beta version.

This is a proof-of-concept with some known bugs, f.e. HTML special chars are not encoded correctly. But this can be fixied easily. First, it is required that the users can choose a property file to edit. This can be realized with a small Java class I have created (based on a stackoverflow.com answer):

package ch.hasselba.xpages.localization;

import java.util.Vector;
import javax.faces.context.FacesContext;
import lotus.domino.Database;
import lotus.domino.Document;
import lotus.domino.NoteCollection;
import lotus.domino.NotesException;

public class DesignElements {

    private final String EMPTY_STRING = "";
    private final String[] FLAG_PROPERTIES = { "gC~4K2P", "gC~4K2" };
    private final String FIELD_$FLAGS = "$Flags";
    private final String FIELD_TITLE = "$TITLE";

    /**
     * returns Vector containing all property files of a database
     * 
     * No error handling included!
     * 
     * @category Domino
     * @author Sven Hasselbach
     * @category Tools
     * @version 0.2
     */
    public Vector<String> getPropertFileList() {

        FacesContext fc = FacesContext.getCurrentInstance();

        Vector<String> data = new Vector<String>();
        try {

            // get DB
            Database db = (Database) fc.getApplication().getVariableResolver()
                    .resolveVariable(fc, "database");

            // get all design docs
            NoteCollection nc = db.createNoteCollection(false);
            nc.selectAllDesignElements(true);
            nc.buildCollection();

            // process all notes
            String noteId = "";
            noteId = nc.getFirstNoteID();

            Document doc = null;
            // 
            while (!(EMPTY_STRING.equals(noteId))) {

                // get design doc
                doc = db.getDocumentByID(noteId);

                // check if its a property file
                for (int i = 0; i < FLAG_PROPERTIES.length; i++) {
                    if (FLAG_PROPERTIES[i].equals(doc
                            .getItemValueString(FIELD_$FLAGS))) {
                        // add to Vector
                        data.add(doc.getItemValueString(FIELD_TITLE));
                    }
                }

                // next one
                noteId = nc.getNextNoteID(noteId);

                // recycle doc
                recycleObject(doc);
            }

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

        return data;

    }

    /**
     * recycles a domino document instance
     * 
     * @param lotus
     *            .domino.Base obj to recycle
     * @category Domino
     * @author Sven Hasselbach
     * @category Tools
     * @version 1.1
     */
    public static void recycleObject(lotus.domino.Base obj) {
        if (obj != null) {
            try {
                obj.recycle();
            } catch (Exception e) {
            }
        }
    }
}

The difference to the answer on stackoverflow.com is the additional property flag I have added to this class (I know it would be better to check the flags instead of comparing the strings, but it seems to work this way).

Next step is to load the selected property file. This can be done with a managed bean and a helper class for the entries itself. Here is the helper class:

package ch.hasselba.xpages.localization;

import java.util.UUID;

/**
 * Locale Entry of a locale file
 * 
 * @author Sven Hasselbach
 * @category Localization
 * @version 1.0
 * 
 */

public class LocaleEntry {

    private String id;
    private String name;
    private String value;

    /**
     * initializes the object and
     * sets an unique identifier
     */
    public LocaleEntry(){
        this.id = UUID.randomUUID().toString();
        this.name = "";
        this.value = "";
    }

    /**
     * returns unique identifier of the object
     * @return String unique id
     */
    public String getId() {
        return id;
    }

    /**
     * sets the unique identifier of the entry
     * @param id String unique id
     */
    public void setId(final String id) {
        this.id = id;
    }

    /**
     * returns the name of the entry
     * @return String
     */
    public String getName() {
        return name;
    }

    /**
     * sets the name of the entry
     * @param String
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * returns the value of the entry
     * @return String
     */
    public String getValue() {
        return value;
    }

    /**
     * sets the value of the entry
     * @param value
     */
    public void setValue(String value) {
        this.value = value;
    }

}

And here comes the “Loader” class:

package ch.hasselba.xpages.localization;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Vector;
import com.ibm.xsp.context.FacesContextEx;

/**
 * "Loader" class to access the entries
 * of a .property file
 * 
 * @author Sven Hasselbach
 * @version 1.0
 */
public class Locale {

    private Vector<LocaleEntry> locales;

    /**
     * @return vector containing the LocalEntry objects
     */
    public Vector<LocaleEntry> getLocales() {
        return locales;
    }

    /**
     * sets vector containing the LocalEntry objects
     * @param Vector
     */
    public void setLocales(Vector<LocaleEntry> locales) {
        this.locales = locales;
    }

    /**
     * wrapper for the static method call
     * 
     * @param fileName name of the property file to load
     */
    public void loadFile(final String fileName) {
        Map<String, String> m = getPropertiesFromFile(fileName);
        this.locales = parseMap(m);
    }

    /**
     * loads a property file and parses it to a key/value map
     * 
     * @param fileName name of the property file to load
     * @return Map containing the key/values
     * 
     * The loading routine is shamelessly copied from Ulrich Krause:
     * http://openntf.org/XSnippets.nsf/snippet.xsp?id=access-.properties-files
     */
    public static Map<String, String> getPropertiesFromFile(String fileName) {
        Properties prop = new Properties();

        try {
            prop.load(FacesContextEx.getCurrentInstance().getExternalContext()
                    .getResourceAsStream(fileName));
            Map<String, String> map = new HashMap<String, String>((Map) prop);

            return map;

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

    }

    /**
     * parses a property map and create a vector
     * with LocalEntry objects
     * 
     * @param map to parse
     * @return Vector containing the LocalEntry objects
     */
    public static Vector<LocaleEntry> parseMap(final Map<String, String> map) {

        // init new vector
        Vector<LocaleEntry> localEntries = new Vector<LocaleEntry>();
        String key;
        String value;

        // get key set for iteration
        Iterator<?> it = map.keySet().iterator();

        while (it.hasNext()) {

            // extract key & value
            key = (String) it.next();
            value = (String) map.get(key);

            // create new entry and add to vector
            LocaleEntry lEntry = new LocaleEntry();
            lEntry.setName(key);
            lEntry.setValue(value);
            localEntries.add(lEntry);

        }

        // return vector
        return localEntries;

    }

    /**
     * dumps current object data to console
     * Just for debugging
     * 
     * @category Debug
     */
    public void dump() {
        for (int i = 0; i < this.locales.size(); i++) {
            LocaleEntry e = this.locales.get(i);
            System.out.println(e.getId() + " - " + e.getName() + " - "
                    + e.getValue());
        }

    }

}

Now the new managed bean can be added to the faces-config.xml:

<managed-bean>
  <managed-bean-name>locale</managed-bean-name>
  <managed-bean-class>ch.hasselba.xpages.localization.Locale</managed-bean-class>
  <managed-bean-scope>session</managed-bean-scope>
</managed-bean>

The resulting XPage can look like this:

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

    Property File:&#160;
    <xp:comboBox id="comboBoxFile" value="#{sessionScope.PropertyFile}">
        <xp:selectItems>
            <xp:this.value>
            <![CDATA[#{javascript:
               importPackage(ch.hasselba.xpages.localization);
               ch.hasselba.xpages.DesignElements().getPropertFileList();
            }]]>
            </xp:this.value>
        </xp:selectItems>
    </xp:comboBox>
    &#160;
    &#160;
    <xp:button value="Load" id="buttonLoadFile">
        <xp:eventHandler event="onclick" submit="true"
            refreshMode="complete">
            <xp:this.action>
               <![CDATA[#{javascript:
                  importPackage(ch.hasselba.xpages.localization);
                  var bean = facesContext.getApplication().getVariableResolver()
                     .resolveVariable(facesContext, "locale");
                  bean.loadFile(sessionScope.get("PropertyFile") )}]]>
            </xp:this.action>
        </xp:eventHandler>
    </xp:button>
    <hr />
    <xp:label id="labelSelecteFile">
        <xp:this.value>
           <![CDATA[#{javascript:sessionScope.get("PropertyFile")}]]>
        </xp:this.value>
    </xp:label>
    <xp:br />
    <xp:br />
    <xp:repeat id="repeat1" rows="9999" var="lMap" value="#{locale.locales}">
        <xp:label value="#{lMap.name}" id="labelMapName" />
        <xp:inputText id="inputTextValue" value="#{lMap.value}" />
        <xp:br />
    </xp:repeat>

</xp:view>

And this is how it looks in the browser:

The next article describes the procedure to save the properties back the database.

XPages: Lost in Translation

18. September 2012 Posted by Sven Hasselbach

The localization feature is really nice and helps a lot, but you also can have some trouble using it. The first problem is that the language codes which are used in XPages are different from the language codes in java.util.Locale.

This SSJS code for example will not work:

var locale = new java.util.Locale( "fr_BE" );
context.setLocale( locale );
context.reloadPage();

It will not work because the java.util.Locale object returns “fr_be“, but the PageDetails are set to “fr_BE“, and that’s why the setting the language does not work.

<xp:text escape="true" id="computedFieldLocale">
   <xp:this.value>
      <![CDATA[#{javascript:
         var locale = new java.util.Locale( "fr_BE" );
         locale.getLanguage()}]]>
      </xp:this.value>
</xp:text>

[This SSJS Code returns "fr_be"]

[In PageDetails "fr_BE" is used]

But if you set the locale string manually with the setLocaleString() method of the context object, it works as designed.

The second problem I ran into is the aggressive transformation of all texts on a XPage. I copied a simple CSS Style definition from another database, but after copy/pasting it I was unable to see any changes in the browser, as soon I switched to another than the default language. The CSS style didn’t work anymore:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
   <style>
      .red { color:rgb(255,0,0 ) }
   </style>
   <p>TEXT</p>
</xp:view>

[Default Language, it worked]

[Other Language, not working anymore]

The CSS style was transformed by the localization feature and was now “translated”. In the generated HTML code, the tag looked like this:

<style>[en| .red { color:rgb(255,0,0 ) } ]</style>

Same result for directly copied JavaScript-Code, Text etc. (as shown in the screenshot above).

But I still love this feature. It makes developers life a lot easier.

XPages: Access resources and their content

15. Juli 2012 Posted by Sven Hasselbach

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";

Quick-n-Dirty: HTML5 UIComponents without effort

17. Mai 2012 Posted by Sven Hasselbach

Obviously it was Chris Toohey who first has discovered the way of manipulation UIComponents with the tagName attribute: http://www.dominoguru.com/pages/xpage_xptext_tagName_options.html

 

The xp:text – element can easily manipulated to add HTML5 functionality to a XPages. By overriding the property tagName, the component can be accessed like every other UI component in the component tree.

Normally the designer only allows a small list of choices for the tagName property:

But this can be easily overwritten by manually editing the tagName attribute in the source and allows to change the generated HTML element to whatever you want.

Here is a small example for a HTML5 progessbar which can be accessed like every other UIComponent:

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

   <xp:text escape="false" id="pBar" disableTheme="true">
      <xp:this.value><![CDATA[#{javascript:""}]]></xp:this.value>
      <xp:this.tagName>
         <![CDATA[#{javascript:"progress"}]]>
      </xp:this.tagName>
      <xp:this.attrs>
         <xp:attr name="value">
            <xp:this.value><![CDATA[#{javascript:
               if( !sessionScope.containsKey("pBar") )
                  sessionScope.put("pBar", 0 );

                var value = sessionScope.get("pBar");

                if( value > 100 ) value = 0;

                sessionScope.put("pBar", (value+10) );

                value
               }]]></xp:this.value>
         </xp:attr>
         <xp:attr value="100" name="max"></xp:attr>
      </xp:this.attrs>
   </xp:text>

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

</xp:view>

The generated HTML Tag looks like this and can for example be refreshed with a partial refresh etc.

<progress id="view:_id1:pBar" value="40" max="100"></progress>

Bug: Invalider Java-Code durch berechnete Tag-Attribute

17. Februar 2012 Posted by Sven Hasselbach

António A Ramos hat einen interessanten Bug entdeckt: Werden die Attribute eines HTML-Tags im Designer berechnet, wird die XPage nicht mehr verarbeitet und ein Internal Server Error tritt auf

So wird der folgende HTML-Tag ordnungsgemäß gerendert…

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

… jedoch ist eine Berechung nicht zulässig:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
   <li data-theme="#{javascript:'e'}"></li>
</xp:view>

Der Grund ist dabei die Umsetzung in den Java-Code, denn dieser verwendet das Attribute ungeprüft als Variablenname – was natürlich fehl schlagen muss.

So sieht der generierte Java-Code aus:

Ein Workaround ist, den generierten Java-Code direkt im Designer zu ändern und fehlerhafte Variablendeklaration von Hand zu bereinigen – allerdings muss der Vorgang nach jedem Build wiederholt werden.

“It’s not a feature, it’s a bug!”

10. Februar 2012 Posted by Sven Hasselbach

In meinem letzten Beitrag habe ich einen Bug entdeckt, den ich an dieser Stelle noch etwas ausführlicher darstellen möchte, denn es handelt sich hierbei nicht um ein normales Verhalten von JSF, sondern schlichtweg um einen Bug während der Transformation nach Java.

Im Vorfeld möchte ich jedoch auf einen sehr guten Artikel von Paul Withers aufmerksam machen, in dem ausführlich dargestellt wird, wie es sein müsste:

http://www.intec.co.uk/xpages-bindings-when-runs-at-page-load/

Der Einfachheit halber greife ich das von Paul gegebene Beispiel auf, um den Bug zu verdeutlichen. Ergänzt man nämlich den Code um Anführungszeichen, dann wird der “On Page Load“-Code nicht mehr ausgeführt:

<xp:text id="computedField2" escape="true"
 value="You are logged in as '${javascript:@UserName()}'.
The fields id is #{id:computedField1}"></xp:text>

[Fett: Der "On Page Load"-Code // In Rot: Die zusätzlichen Anführungszeichen]

Das Ergebnis ist dann folgendes:

Zurückzuführen ist das auf einen Fehler bei der Transformierung, der generierte Javacode sieht wie folgt aus:

Dies ist ein Bug im Designer, denn jedwede Form der ${}-Syntax wird ungeprüft als “On Page Load” interpretiert. So wird der folgende Code trotz Fehler in EL übersetzt…

… hingegen wird diese Variante ordnungsgemäß als Fehler markiert und lässt sich nicht speichern:

Irrtümlicherweise habe ich in meinem vorigen Artikel weitere Beispiele aufgeführt, die Code enthalten, der ohne das Anführungszeichen ausgeführt wird. Dies war im Zuge des Schreiben des letzten Artikels, als ich noch weitere Test gemacht habe. Hier ist das Verhalten natürlich JSF-konform und der Code wird ordnungsgemäß ausgeführt.

Meine Beobachtung bezüglich der fehlerhaften Transformation jedoch bezieht sich nicht nur auf Output Scripts, sondern um jede Art des Value Bindings: Sobald eine x-beliebige Kombination von ${} (auch über mehrere Zeilen etc.) vorkommt, tritt der Fehler auf.

Application-Properties mittels SSJS auslesen

13. November 2011 Posted by Sven Hasselbach

Um Serverseitig zu ermitteln, welche Dojo-Version verwendet wird, kann folgender Code verwendet werden:

<xp:label id="lblJSVersion">
   <xp:this.value><![CDATA[#{javascript:var reqParam =
      new com.ibm.xsp.context.RequestParameters ( facesContext );
      reqParam.getLibraryVersion()}]]>
   </xp:this.value>
</xp:label>

[Code liefert unter 8.5.3 in der Standard-Einstellung "1.6.1" zurück]

Um zu ermitteln, ob die Clientseitige Validierung aktiviert bzw. deaktiviert ist, liefert die Methode isClientSideValidation() das gewünschte Ergebnis:

<xp:label id="lblIsCSValidation">
   <xp:this.value><![CDATA[#{javascript: var reqParam =
      new com.ibm.xsp.context.RequestParameters ( facesContext );
      reqParam.isClientSideValidation()}]]>
   </xp:this.value>
</xp:label>

[Liefer true bzw. false zurück, je nach Einstellung der Datenbank]

Welche Komprimierungseinstellung verwendet wird, kann so ermittelt werden:

<xp:label id="lblCompressMode">
   <xp:this.value><![CDATA[#{javascript:var reqParam =
      new com.ibm.xsp.context.RequestParameters ( facesContext );
      reqParam.getCompressMode()}]]>
   </xp:this.value>
</xp:label>

[Liefert z.B. "gzip-nolength" zurück, je nach Einstellung der Datenbank]

Ob überhaupt Dojo Verwendung findet, bzw. die XSPDojoLite.js-Version, liefert folgender Code:

<xp:label id="lblJsLibrary">
   <xp:this.value><![CDATA[#{javascript:var reqParam =
      new com.ibm.xsp.context.RequestParameters ( facesContext );
      var jsLib = reqParam.getJsLibrary();

      switch(jsLib){
      case 1:
         return "dojo";
      break;
      case 2:
         return "lite";
      break;
      default:
         return "none";
      break;
      }}]]>
   </xp:this.value>
</xp:label>

Java not found

18. Oktober 2011 Posted by Sven Hasselbach

Tippfehler werden von Domino (8.5.2) hart bestraft! Java kann nicht mehr gefunden werden!

Screenshot: Java Not Found

 

Die Hashmap muß natürlich “HashMap” heissen, und nicht “Hashmap”. Dann klappts auch…

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
    <xp:scriptBlock id="scriptBlock1"
        value="#{javascript:
            var data:java.util.HashMap = new java.util.HashMap();}">
    </xp:scriptBlock>
</xp:view>