Posts Tagged: ‘9.0’

Quick-n-Dirty: Hotfix for DateTimeHelper

12. Juni 2017 Posted by Sven Hasselbach

This weekend I stumbled over a bug of the DateTimeHelper: If the value of the field is empty, no actions and/or action listeners connected with a managed bean will be executed anymore.

Here is an example of a small XPage to illustrate the problem:

<?xml version="1.0" encoding="UTF-8"?><?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
 
    <xp:label 
        value="#{javascript:java.lang.System.currentTimeMillis()}" id="labelNow" />

     <xp:inputText id="inputTextDT" value="#{myBean.valueDT}">
         <xp:this.converter>
             <xp:convertDateTime type="date" />
         </xp:this.converter>
         <xp:dateTimeHelper />
     </xp:inputText>

    <xp:button id="button" value="OK">
        <xp:eventHandler
            event="onclick"
            submit="true"
            refreshMode="partial"
            refreshId="labelNow"
            actionListener="#{myBean.action}" />
     </xp:button>

</xp:view>

It does not matter if you set the disableValidators property for the text field to true, even an immediate=true won’t help here. The reason for the problem is that the renderer of the dateTimeHelper always uses the attached converter and fails with a null pointer exception if the value is empty (this infringes the JSF specification, but IBM has implemented it this way).

The workaround for this problem is to overwrite the existing renderer class and handle the NPE by yourself:

package ch.hasselba.xpages.renderer;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.ConverterException;
public class DateTimeHelperRenderer
    extends com.ibm.xsp.renderkit.dojo.DateTimeHelperRenderer{

    public Object getConvertedValue(FacesContext fc, UIComponent uiComponent, Object obj)
        throws ConverterException  {

          Object result = super.getConvertedValue(fc, uiComponent, obj);

          if( result == null )
            return new Object();

          return result;
    }
}

The renderer must now be registered in faces-config.xml:

<?xml version="1.0" encoding="UTF-8"?><?xml version="1.0" encoding="UTF-8"?>
<faces-config>
  <render-kit>
    <renderer>
      <component-family>javax.faces.Input</component-family>
      <renderer-type>com.ibm.xsp.DateTimeHelper</renderer-type>
      <renderer-class>ch.hasselba.xpages.renderer.DateTimeHelperRenderer</renderer-class>
    </renderer>
  </render-kit>
</faces-config>

Now the problem is solved, the managed bean’s action get executed even if the value is empty.

Testing XPages

16. September 2015 Posted by Sven Hasselbach

When testing XPages with Selenium, you can easily pre-generate the JUnit test code with the browser plugin. But when you then change the structure of the XPage (f.e. by moving the components from an XPage to a custom control), all the IDs of the JUnit test will not work anymore.

That’s why it is better to use CSS selectors to access the generated fields:

driver.findElements(By.cssSelector("input[id*='idOfTheComponent']"));

With the selector „id*=’idOfTheComponent'“ you can access the elements by their component id, idependently of their full generated client id.

Here is an example with a small XPage with a radio group and a listbox:

 

A simple XPage to test (SimpleDemo.xsp)

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

    <xp:radioGroup
        id="radioGroupSimpleDemo"
        defaultValue="1">
        <xp:selectItem itemLabel="1" itemValue="1" />
        <xp:selectItem itemLabel="2" itemValue="2" />
    </xp:radioGroup>


    <xp:listBox id="listBoxSimpleDemo" multiple="true">
        <xp:selectItem itemLabel="1" itemValue="1" />
        <xp:selectItem itemLabel="2" itemValue="2" />
        <xp:selectItem itemLabel="3" itemValue="3" />
    </xp:listBox>
 
</xp:view>

 The JUnit Test

package ch.hasselba.xpages.test.seleniumdemo;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.List;
import java.util.concurrent.TimeUnit;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.support.ui.Select;

public class SimpleDemo {

	private WebDriver driver;
	private String baseUrl = "http://127.0.0.1/WebTestDemo.nsf/SimpleDemo.xsp";
	private StringBuffer verificationErrors = new StringBuffer();

	@Before
	public void setUp() throws Exception {
		driver = new FirefoxDriver();
		driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
	}

	@After
	public void tearDown() throws Exception {
		driver.quit();
		String verificationErrorString = verificationErrors.toString();
		if (!"".equals(verificationErrorString)) {
			fail(verificationErrorString);
		}
	}

	@Test
	public void testDemo() throws Exception {
		reloadPage();
		List elemRadio = driver.findElements(By
				.cssSelector("input[id*='radioGroupSimpleDemo']"));
		assertTrue(elemRadio.get(0).isSelected());
		assertFalse(elemRadio.get(1).isSelected());

		Select select = new Select(driver.findElement(By
				.cssSelector("select[id*='listBoxSimpleDemo']")));
		List listSelect = select.getOptions();
		for (WebElement listElem : listSelect) {
			assertFalse(listElem.isSelected());
			assertTrue(listElem.isEnabled());
		}

	}

	public void reloadPage() {
		driver.get(baseUrl);
	}
}

The Maven pom.xml file

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
    http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>ch.hasselba.xpages.test</groupId>
    <artifactId>seleniumdemo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>seleniumdemo</name>
    <url>http://hasselba.ch</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>2.46.0</version>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
            <version>1.3</version>
        </dependency>
        <dependency>
            <groupId>pl.pragmatists</groupId>
            <artifactId>JUnitParams</artifactId>
            <version>1.0.4</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

XPages: A ClientSide State

13. September 2015 Posted by Sven Hasselbach

I have created a ClientSide State for XPages, which allows horizontal scaling of XPages applications with a single click.

After installing the OSGi Plugin on the servers and the DDE, you can activate it with a single click:

2015-09-13 20_41_02-XPage Properties Editor - ClientStateDemo - IBM Domino Designer

Then, the State of the XPages application is stored on client side (in the field „$$clientstate„), and not on the server anymore. The State is AES-128 encrypted and uses some random bytes for a higher security.

2015-09-13 20_52_11-Quelltext von_ http___localhost_ClientStateDemo.nsf_Test.xsp

You can find the code on GitHub.

P.S.

Parts of the code are shamelessly stolen from Jesse.

XPages: SSJS, EL and Bindings

2. Juli 2015 Posted by Sven Hasselbach

Because of reasons you should already know I avoid the use of SSJS in my XPages applications, but there are still some parts which can be easy realized in SSJS, but with EL only with a lot of effort. One of this things is accessing properties of a component which has only a getter or a setter – this will not work when using a binding.

Let’s look for example at repeat control which is bound to the variable repeat. It can be easily accessed everywhere in SSJS, EL or Java, and it is much faster then searching the component in the tree. Here it is accessed in a label:

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

    <xp:label
        value="#{repeat.rowCount}"
        id="labelCount">
    </xp:label>
    
    <xp:repeat
        id="repeatDemo"
        rows="30"
        binding="#{repeat}">
        <xp:this.value><![CDATA[#{javascript:[1,2,3,4,5]}]]></xp:this.value>
    </xp:repeat>
    
</xp:view>

But this will lead into an error, because the XspDataIterator component has only a getter method for the rowCount property, not a corresponding setter.

2015-07-01 23_11_16-Runtime Error

When changing the code to a SSJS method call, it works without any problems:

    <xp:label
        value="#{javascript:repeat.getRowCount()}"
        id="labelCount">
    </xp:label>

The reason for this behaviour can be found deep down in the underlying JSF classes: The BeanInfoManager ignores all methods without a “valid” getter/setter methods pair during the inspection of a class, and doesn’t add it to the available properties for the PropertyResover. The rows property for example has a pair of those methods, that’s why this works without any problem:

<xp:label
    value="#{repeat.rows}"
    id="labelCount">
</xp:label>

 

XPages: An optimized JavaScript Resource Renderer

21. Juni 2015 Posted by Sven Hasselbach

Ferry Kranenburg created a nice hack to solve the AMD loader problem with XPages and Dojo, and because of the missing ability to add a resource to the bottom of an XPage by a property, I have created a new JavaScriptRenderer which allows to control where a CSJS script will be rendered.

The renderer has multiple options:

  • NORMAL – handles the CSJS resource as always
  • ASYNC – loads the script in an asynchronous way (with an own script tag)
  • NOAMD – adds the no amd scripts around the resource
  • NORMAL_BOTTOM – adds the script at the bottom of the <body> tag
  • ASYNC_BOTTOM – async, but at the end of the generated HTML page
  • NOAMD_BOTTOM – at the end, with the surrounding no amd scripts

To use the normal mode, you don’t have to change you resource definition. If you want to use the other modes, you have to change the content type of the resource with one of the entries in the list above. This for example would add a script block to the end of the page, including the non amd script blocks around it:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
    <xp:this.resources>
        <xp:script clientSide="true" type="NOAMD_BOTTOM">
            <xp:this.contents><![CDATA[alert("Hello World!");]]></xp:this.contents>
        </xp:script>
    </xp:this.resources>
</xp:view>

2015-06-21 11_04_09

Here is the code for the resource renderer:

package ch.hasselba.xpages;

import java.io.IOException;
import java.util.Iterator;
import java.util.Map;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;

import com.ibm.xsp.component.UIViewRootEx;
import com.ibm.xsp.renderkit.html_basic.ScriptResourceRenderer;
import com.ibm.xsp.resource.Resource;
import com.ibm.xsp.resource.ScriptResource;
import com.ibm.xsp.util.JSUtil;

public class OptimizedScriptResourceRenderer extends ScriptResourceRenderer {
    private static final String TYPE = "type";
    private static final String SCRIPT = "script";
    private static final String CSJSTYPE = "text/javascript";
    private boolean isBottom = false;
    
    private static enum Mode {
        NORMAL, ASYNC, NOAMD, ASYNC_BOTTOM, NOAMD_BOTTOM, NORMAL_BOTTOM
    }

    public void encodeResourceAtBottom(FacesContext fc,
            UIComponent uiComponent, Resource resource) throws IOException {
        isBottom = true;
        encodeResource(fc, uiComponent, resource);
        isBottom = false;
    }

    @Override
    public void encodeResource(FacesContext fc, UIComponent uiComponent,
            Resource resource) throws IOException {

        ScriptResource scriptResource = (ScriptResource) resource;
        ResponseWriter rw = fc.getResponseWriter();
        String type = scriptResource.getType();
        String charset = scriptResource.getCharset();
        String src = scriptResource.getSrc();

        Mode mode = Mode.NORMAL;
        try{
            mode = Mode.valueOf( type );
        }catch(Exception e){};

        if (mode == Mode.NORMAL || mode == Mode.NORMAL_BOTTOM ) {
            normalBottomJSRenderer( fc, uiComponent, scriptResource, (mode == Mode.NORMAL_BOTTOM), type );
        } else {
            if (mode == Mode.ASYNC || mode == Mode.ASYNC_BOTTOM) {
                asyncJSRenderer(fc, uiComponent, scriptResource, (mode == Mode.ASYNC_BOTTOM), rw, type,
                        charset, src );
            }else if (mode == Mode.NOAMD || mode == Mode.NOAMD_BOTTOM ) {
                noAMDJSRenderer(fc, uiComponent, scriptResource, (mode == Mode.NOAMD_BOTTOM) , rw, 
                        type, charset, src);
            }

        }

    }

    private void normalBottomJSRenderer(FacesContext fc,UIComponent uiComponent,
            ScriptResource scriptResource, final boolean addToBottom, final String type ) throws IOException {
        
        if( addToBottom && !isBottom )
            return;
        scriptResource.setType(null);
        super.encodeResource(fc, uiComponent, scriptResource);
        scriptResource.setType(type);
        
    }
    private void asyncJSRenderer(FacesContext fc,
            UIComponent uiComponent, ScriptResource scriptResource, 
             final boolean addToBottom, ResponseWriter rw, final String type, final String charset,
            final String src) throws IOException {
        
        if( addToBottom && !isBottom )
            return;
        
        Map<String, String> attrs = null;
        String key = null;
        String value = null;
        String id = "";

        if (scriptResource.getContents() == null) {
            attrs = scriptResource.getAttributes();
            if (!attrs.isEmpty()) {
                StringBuilder strBuilder = new StringBuilder(124);
                for (Iterator<String> it = attrs.keySet().iterator(); it
                        .hasNext();) {
                    key = it.next();
                    value = attrs.get(key);
                    strBuilder.append(key).append('(').append(value)
                            .append(')');
                }
                id = strBuilder.toString();
            }

            // check if already added
            UIViewRootEx view = (UIViewRootEx) fc.getViewRoot();

            String resId = "resource_" + ScriptResource.class.getName() + src
                    + '|' + type + '|' + charset + id;
            if (view.hasEncodeProperty(resId)) {
                return;
            }
            view.putEncodeProperty(resId, Boolean.TRUE);

        }
        if (!scriptResource.isClientSide()) {
            return;
        }

        rw.startElement(SCRIPT, uiComponent);
        JSUtil.writeln(rw);
        rw.write("var s = document.createElement('" + SCRIPT + "');");
        JSUtil.writeln(rw);
        rw.write("s.src = '" + src + "';");
        JSUtil.writeln(rw);
        rw.write("s.async = true;");
        JSUtil.writeln(rw);
        rw.write("document.getElementsByTagName('head')[0].appendChild(s);");
        JSUtil.writeln(rw);
        rw.endElement(SCRIPT);
        JSUtil.writeln(rw);
    }

    
    private void noAMDJSRenderer(FacesContext fc,
             UIComponent uiComponent,ScriptResource scriptResource,
            final boolean addToBottom, ResponseWriter rw, final String type, final String charset,
            final String src ) throws IOException {
        
        if( addToBottom && !isBottom )
            return;

        // write the "disable AMD" script
        rw.startElement(SCRIPT, uiComponent);
        rw.writeAttribute(TYPE, CSJSTYPE, TYPE);
        rw.writeText(
                        "'function'==typeof define&&define.amd&&'dojotoolkit.org'==define.amd.vendor&&(define._amd=define.amd,delete define.amd);",
                        null);
        rw.endElement(SCRIPT);
        JSUtil.writeln(rw);

        // write the normal CSJS
        scriptResource.setType(null);
        super.encodeResource(fc, uiComponent, scriptResource);
        scriptResource.setType(type);
        // write the "reenable AMD" script
        rw.startElement(SCRIPT, uiComponent);
        rw.writeAttribute(TYPE, CSJSTYPE, TYPE);
        rw
                .writeText(
                        "'function'==typeof define&&define._amd&&(define.amd=define._amd,delete define._amd);",
                        null);
        rw.endElement(SCRIPT);
        JSUtil.writeln(rw);

    }
}

The ViewRenderer must also be modified, otherwise it is not possible to add the resources at the bottom of the <body> tag:

package ch.hasselba.xpages;

import java.io.IOException;
import java.util.List;

import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.render.Renderer;

import com.ibm.xsp.component.UIViewRootEx;
import com.ibm.xsp.render.ResourceRenderer;
import com.ibm.xsp.renderkit.html_basic.ViewRootRendererEx2;
import com.ibm.xsp.resource.Resource;
import com.ibm.xsp.resource.ScriptResource;
import com.ibm.xsp.util.FacesUtil;

public class ViewRootRendererEx3 extends ViewRootRendererEx2 {

    protected void encodeHtmlEnd(UIViewRootEx uiRoot, ResponseWriter rw)
            throws IOException {
        FacesContext fc = FacesContext.getCurrentInstance();

        List<Resource> resources = uiRoot.getResources();
        for (Resource r : resources) {
            if (r instanceof ScriptResource) {
                ScriptResource scriptRes = (ScriptResource) r;
                if (scriptRes.isRendered()) {
                    Renderer renderer = FacesUtil.getRenderer(fc, scriptRes.getFamily(), scriptRes.getRendererType());
                    ResourceRenderer resRenderer = (ResourceRenderer) FacesUtil.getRendererAs(renderer, ResourceRenderer.class);
                    if( resRenderer instanceof OptimizedScriptResourceRenderer ){
                        ((OptimizedScriptResourceRenderer) resRenderer).encodeResourceAtBottom(fc, uiRoot, r);
                    }
                }
            }
        }

        rw.endElement("body");
        writeln(rw);
        rw.endElement("html");
    }

}

To activate the new renderes, you have to add them to the faces-config.xml:

<?xml version="1.0" encoding="UTF-8"?>
<faces-config>
  <render-kit>
    <renderer>
      <component-family>com.ibm.xsp.resource.Resource</component-family>
      <renderer-type>com.ibm.xsp.resource.Script</renderer-type>
      <renderer-class>ch.hasselba.xpages.OptimizedScriptResourceRenderer</renderer-class>
    </renderer>
    <renderer>
      <component-family>javax.faces.ViewRoot</component-family>
      <renderer-type>com.ibm.xsp.ViewRootEx</renderer-type>
      <renderer-class>ch.hasselba.xpages.ViewRootRendererEx3</renderer-class>
    </renderer>
  </render-kit>
</faces-config>

Needless to say that this works in Themes too.

xsp.application.context.proxy

17. Juni 2015 Posted by Sven Hasselbach

Just a reminder for myself: To use a CDN for XPage resources, you can add a leading slash to the xsp.application.context.proxy property.

xsp.application.context.proxy=/cdn.hasselba.ch

2015-06-17 10_52_08-view-source

 

XPages: Running Google’s Chrome V8 Javascript Engine (2)

10. April 2015 Posted by Sven Hasselbach

A while ago I tried to run Google’s V8 Javascript engine on top of XPages, and today I found the reason why my server crashed after the first click: I have tried to load the engine only once (statically), and that killed Domino completly.

Today I moved the code back into the processAction method, and now it works without problems.

package ch.hasselba.xpages.jav8;

import javax.faces.event.AbortProcessingException;
import javax.faces.event.ActionEvent;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

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

    public void processAction(ActionEvent actionEvent)
            throws AbortProcessingException {

        ScriptEngineManager factory = new ScriptEngineManager();
        ScriptEngine engine = factory.getEngineByName("jav8");

        try {
            System.out.println(engine.getClass().getCanonicalName());
            engine.eval("var i=1+1;");
            System.out.println("i = " + engine.get("i"));
        } catch (ScriptException ex) {
            ex.printStackTrace();
        }
    }
}

Rest & Security: More about the DominoStatelessTokenServlet

25. Februar 2015 Posted by Sven Hasselbach

During the last days I have refined the DominoStatelessTokenServlet a little bit. It is now a pre-beta release, and I think it is time to explain some details about it. While it is still a proof-of-concept, it demonstrates how a stateless authentication can easily be implemented. A lot of testing is still required until it is ready for production use, but I think it provides really cool things for the domino environment.

First, it fixes the problematic 200er HTTP response code when an authentication was not successfull. Then it introduces a higher security level for web based applications, because the authentication token is only transferred in the HTTP headers: A CSRF attack as shown here is not possible anymore. The authentication works accross multiple servers / clusters, which can become interesting for example when you want to share the authentication securely between a Bluemix hosted application and your companies hosted infrastructure; the token is created from a server running in your company, and is then used to identify a user in the cloud-based application (It’s a real authentication, not a “misused authorization” like OAuth).

The token can also be safely stored in an mobile app: When the device gets lost, the user credentials are not compromised. And it allows to create different tokens for different applications for the same user (currently not implemented).

As a short demonstration, I have added a Angular JS example (with a hardcoded token) to  show how it works in practise: An AJAX request is sent to the servlet, and the JSON response contains the name of the current Domino user.

The HTML page is not really complicated, it will just show the returned username:

<!doctype html>
<html ng-app>
   <head>
      <title>Hello AngularJS</title>
      <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js"></script>
      <script src="hello.js"></script>
   </head>
   <body>
      <div ng-controller="Hello">
         <p>Hello {{greeting.username}}!</p>
      </div>
   </body>
</html>

The “Hello” controller which performs the AJAX request adds the “X-AUTH-TOKEN” to the HTTP headers of the request:

function Hello($scope, $http) {

   // add a valid token to the headers
   $http.defaults.headers.common['X-AUTH-TOKEN'] = 
     'MTQyNDI2OTE0NDgxNCFUVGVzdFVzZXIwMQ==!HyC1mnvvdaneLaW0Wn48kZ1MaTrdowr1e4nWBRWRX8Y=';

   // load the data
   $http.get('http://localhost/token/validate/').
      success(function(data) {
         $scope.greeting = data;
      });

}

And this is the result:

StatelessToken 01 - Validate Token

The Token consist of two parts: The data part, and the Hmac hash of the data. Both are Base64 encoded, and when the data part is decoded, you can currently see the username and the timestamp of the token generation:

  • Encoded
MTQyNDI2OTE0NDgxNCFUVGVzdFVzZXIwMQ==
  • Decoded
1424269144814!TTestUser01

Because the data are hashed it is not possible to modify them. The timestamp is validated from the servlet; as soon it is too old, it is not longer valid.

To create a token, the servlet must be currently opened with the username and the password as URL parameters:

http://localhost/token/create/?username=TTestUser01&password=test01

In the response, the newly generated token is added to the HTTP headers:

StatelessToken 02 - Create Token

In the servlet configuration (web.xml), a backend user is defined on whose behalf a lookup to the NAB is made to verify the provided HTTP password.  The password for the hash is also stored there, and the maximum age of the token.

Rest & Security: A Stateless Token Servlet

10. Februar 2015 Posted by Sven Hasselbach

I have uploaded some of my projects to GitHub, including an alpha version of a stateless token servlet.

The servlet has it’s own authentication mechanism (the password is currently not validated), and for developing purposes it uses HTTP GET. In a future release, the token will be transfered as a HTTP header. Additionally, the HTTP method will be changed to POST. Last but not least must the code be optimized. For example there is no recycling implemented at this moment, and there is a dubious bug in the token validation (which was solved by encoding it to Base64 and back again).

  • Generate a Token

To generate a new token, you have to open the servlet with the following URL format:

"/create?username=<CANONICAL USER>&password=<WHATEVER>"

Example for “CN=Sven Hasselbach/OU=Hasselba/O=CH

http://example.com/token/create/?username=CN%3DSven%20Hasselbach%2FOU%3DHasselba%2FO%3DDE&password=XXX

This will return a JSON string containg the newly generated token:

{token: 'MDkgRmViIDIwMTUgMjI6NDk6MTIuMzU2IUNOPVN2ZW4gSGFzc2VsYmFjaC9PVT1IYXNzZWxiYS9PPURF!9s7dSS7F67hSS/lLODZHVM0NsTR4IurkZtjiysGzoWA='}

The Token is a Base64-encoded String containing a Timestamp, the Username and a Hmac Hash and only valid for one hour.

  • Validate a Token

To validate the token, you can use the URL

/validate/?token=<token>

Example:

http://example.com/token/validate/?token=MDkgRmViIDIwMTUgMjI6NDk6MTIuMzU2IUNOPVN2ZW4gSGFzc2VsYmFjaC9PVT1IYXNzZWxiYS9PPURF%219s7dSS7F67hSS%2FlLODZHVM0NsTR4IurkZtjiysGzoWA%3D

If the token is valid, a new “NotesSession” is created for the given user. The server response contains a JSON String with the current “EffectiveUserName”:

{user: 'CN=Sven Hasselbach/OU=Hasselba/O=CH'}

Because the servlet creates its own session, you can be logged in as a different user: The Domino authentication is not affected by the servlet.

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: Running Google’s Chrome V8 Javascript Engine

9. November 2014 Posted by Sven Hasselbach

After answering a question on Stackoverflow.com about the Prototype problematic in the XPages SSJS engine, I thought of running another Javascript engine on top of Domino.

While you can use the JavaScripting API JSR223, I choosed the jav8 project for a test how this can be realized. So I downloaded the Windows binaries to get the required DLL and imported it into a new database. I also imported the source files of the lu.fler.script package to recompile all required classes.

Then, I registered the factory service by creating a javax.script.ScriptEngineFactory file in the /META-INF/services folder and added the line lu.flier.script.V8ScriptEngineFactory.

The package explorer looked like this:

To prevent collisions, I commented out some names in the V8ScriptEngineFactory class:

For a simple test, I decided to invoke the engine manually when clicking on a button on a XPage. To do this, I created a simple ActionListener in Java which loads the JavaScript Engine and evals a simple ” var i = 1+1″. The Javascript variable is then accessed and printed out to the server console.

package ch.hasselba.xpages.jav8;

import javax.faces.event.AbortProcessingException;
import javax.faces.event.ActionEvent;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

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

    static ScriptEngineManager factory = new ScriptEngineManager();
    static ScriptEngine engine = factory.getEngineByName("jav8");

    public void processAction(ActionEvent actionEvent)
            throws AbortProcessingException {
        try {
            System.out.println( engine.getClass().getCanonicalName() );
            engine.eval("var i=1+1;");
            System.out.println( "i = " + engine.get("i") );
        } catch (ScriptException ex) {
            ex.printStackTrace();
        }
    }
}

The XPage to test the ActionListener is rather simple too:

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

    <xp:button
        value="Click Me!"
        id="buttonClickMe">
        <xp:eventHandler
            event="onclick"
            submit="true"
            refreshMode="complete">
            <xp:this.actionListeners>
                <xp:actionListener type="ch.hasselba.xpages.jav8.JAV8Test" />
            </xp:this.actionListeners>
        </xp:eventHandler>
    </xp:button>
    
</xp:view>

When the button is clicked, the V8 engine works as it should:

But now comes a hard problems: It works only once! After doing this, my test server crashes completly. During playing with it, I was able to run it as it should, but I have no idea anymore how I did it. I think it is the DLL and static classes, but I am currently to busy to investigate the problem further. The @Override notations added to the methods (which must removed before the code can be compiled) do not override exitisting ones (I checked the bundled javax.script JAR of the binary package), this does not seem to be the problem. Maybe someone else has an idea?

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:

The Voices Told Me To Do It!

4. Juni 2014 Posted by Sven Hasselbach

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).