wissel.net

Usability - Productivity - Business - The web - Singapore & Twins

By Date: April 2014

You want to move to Domino? You need a plan!


Cloud services are all en vogue, the hot kid on the block and irressitible. So you decided to move there, but you decided your luggage has to come along. And suddenly your realize, that flipping a switch won't do the trick. Now you need to listen to the expert.
The good folks at Amazon have compiled a table that gives you some idea how much it would take to transfer data:
Available Internet Connection Theoretical Min. Number of Days
to Transfer 1TB
at 80% Network Utilization
TB
T1 (1.544Mbps) 82 days
10Mbps 13 days
T3 (44.736Mbps) 3 days
100Mbps 1 to 2 days
1000Mbps Less than 1 day Some stuff for your math
(Reproduced without asking)
Talking to customers gung ho to move, I came across data volumes of 10-400 TB. Now go and check your pipe and do the math. A big bang, just flip the switch migration is out of the picture. You need a plan. Here is a cheat sheet to get you started:
  1. Create a database that contains all information of your existing users and how they will be once provisioned on Domino (If you are certified for IBM SmartCloud migrations IBM has one for you)
  2. Gather intelligence on data size and connection speed. Design your daily batch size accordingly
  3. Send a message to your existing users, where you collect their credentials securely and offer them a time slot for their migration. A good measure is to bundle your daily slots into bigger units of 3-7 days, so you have some wiggle room. Using some intelligent lookup you only present slots that have not been taken up
  4. Send a nice confirmation message with date and steps to be taken. Let the user know, that at cut-over day they can use the new mail system instantly, but it might take a while (replace "while" with "up to x hours" based on your measurements and the mail size intelligence you have gathered) before existing messages show up
  5. When the mailbox is due, send another message to let the user kick off the process (or confirm her consent that it kicks off). In that message it is a good idea to point to learning resources like the "what's new" summary or training videos or classes
  6. Once the migration is completed, send another message with some nice looking stats and thanking for your patience
  7. Communicate, Communicate, Communicate!
The checklist covers the user facing part of your migration. You still have to plan DNS cut-over, routing while moving, https access, redirection on mail links etc. Of course that list also applies for your pilot group/test run.
As usual: YMMV

Posted by on 17 April 2014 | Comments (0) | categories: Cloud Computing Container IBM Notes K8S

Domino Design Pattern: Secret documents


Domino's stronghold is security. However security is only as good as you design it. A frequent requirement in applications is to store a data set that is partially confidential and partially available for a wider audience. When you store these 2 data sets in one document, it isn't too hard to have the confidential information slip out:
  • using the document properties in a Notes client
  • using the document rest service
  • the property control from openNTF
In a nutshell: if you have 2 sets of data with different levels of read access requirements, don't store them in one document. A well working pattern in Domino is the "Secret Document". The following picture illustrates the concept:
Use 2 documents to store 2 sets of information security requirements
The user is presented with one form, but saving the entered data is done in two documents. The documents are cross referenced using the UNID. This can happen two way (as shown in the picture): the public document's UNID is saved in the secret document and vice versa - or - one way, with only the secret ID in the public document. A few pointers:
  • Based on the application's need some of the public data get repeated inside the secret document if that needs to be displayed on its own (e.g. a salary list in an HR application)
  • To avoid data drifting apart the respective data would only get updated in the public document ever and then copied to the secret document. In classic Notes that is done using a on-change agent, while in XPages a session-as-signer code snippet will suffice.
  • For very sensitive data (like even the normal user shall not see), these data sets could be stored in their own encrypted NSF. Then the UNID might not be enough, but the full notes:// url would make more sense
  • In classic Notes the embedded form editor makes the user experience with 2 documents seamless
  • In XPages two (or more) data sources sitting on one page will do the trick
As usual YMMV

Posted by on 17 April 2014 | Comments (2) | categories: IBM Notes XPages

SmartCloud Notes little agent helper


Now that we all drank the Cloud Computing CoolAid, we need to make it work. IBM's SmartCloud Notes looks enticing, since it offers 25G of eMail storage, way beyond what IT departments usually want to commit.
SmartCloud Notes even allows you customisation albeit within clear limits. So before you upload your extension forms you need to plan well.
One of the most unpleasant restrictions is: " No customer agents or scripts will be executed on server ", so no agent, no DOLS tasks. However you can run an agent (or other code) on an on-premises server. The interesting question is: when and how to trigger such code. Looking at the basic iNotes customization article you can find the Custom_Scene_PreSubmit_Lite JavaScript function. This could be the place to launch such a trigger. More on that in the next installment.
This article outlines the receiving end - the stuff that runs on your on-premises server. Instead of running agents, I'll outline a plug-in that allows to process the document submitted. The interface between SCN and this service is a JSON submission in the form of:

{
  "userName": "TestUser@acme.com",
  "action": "DemoTask",
  "unid": "32char-unid"
}

Once the plug-in receives this data, processing can commence. Of course the server (that's the ID the plug-in runs with) needs to have access to the mail file at the task required level. Let's get started:
In a plugin project we define a new servlet:

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
   <extension
         point="org.eclipse.equinox.http.registry.servlets">
      <servlet
            alias="/scntask"
            class="com.notessensei.cloudproxy.TaskServlet"
            load-on-startup="true">
      </servlet>
   </extension>
</plugin>

Then our servlet looks like this:

package com.notessensei.cloudproxy;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import lotus.domino.Base;
import lotus.domino.NotesException;

public class TaskServlet extends HttpServlet {

 private static final long  serialVersionUID = 1L;

 // Number of threads allowed to run concurrently for data sync
 private static final int  THREADPOOLSIZE  = 16;

 // The background executor for talking to the cloud
 private final ExecutorService service    = Executors.newFixedThreadPool(THREADPOOLSIZE);

 // The Cache where we keep our user lookup objects, handle with care!
 private final UserCache   userCache   = new UserCache();

 @Override
 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  // Takes in a JSON String and makes a task object
  InputStream in = req.getInputStream();
  CloudNotesTask it = CloudNotesTask.load(in);
  String result;

  if (it != null) {
   it.setUserCache(this.userCache);
   DocumentTaskFactory.getDocumentTask(it);
   this.service.execute(it);
   resp.setStatus(HttpServletResponse.SC_OK);
   result = "{\"status\" : \"task accepted\"}";
  } else {
   resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
   result = "{\"status\" : \"task failed\"}";
  }

  // Prepare the reply back
  resp.setContentType("application/json");
  PrintWriter out = resp.getWriter();
  out.println(result);
  out.close();

 }

 /**
  * Get rid of all Notes objects
  *
  * @param morituri
  *            = the one designated to die, read your Caesar!
  */
 public static void shred(final Base... morituri) {
  for (Base obsoleteObject : morituri) {
   if (obsoleteObject != null) {
    try {
     obsoleteObject.recycle();
    } catch (NotesException e) {
     // We don't care we want go get
     // rid of it anyway
    } finally {
     obsoleteObject = null;
    }
   }
  }
 }
}
##
The two interesting pieces are the CloudNotesTask and the UserCache. In my sample I use some Apache licensed Google libraries gson and guava. They make Java live much easier. From the guava library the Cache object is used, that automatically retrieves data when requested. The calling class just queries the cache and data acquisition is completely transparent. Look for yourself:

package com.notessensei.cloudproxy;

import java.util.Vector;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import lotus.domino.Directory;
import lotus.domino.DirectoryNavigator;
import lotus.domino.Name;
import lotus.domino.Session;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

/**
 * Keeps the information about the user and the mail file
 * @author stw
 */
public class UserCache {

 private final static String     LOOKUP_VIEW_NAME = "($Users)";
 private final static String     MAILSERVER_FIELD = "MailServer";
 private final static String     MAILFILE_FIELD = "MailFile";

 // TODO CHECK the size and validity of the cache lifespan
 private final static int     MAX_CACHE_ENTRIES = 1000;
 private final static int     EXPIRES_AFTER = 8;
 private final static TimeUnit    UNIT = TimeUnit.HOURS;

 private final LoadingCache<String>,<String> cachedUsers;
 private final Vector<String> serverFields;
 private Session session = null;

 public UserCache() {
       // Fields to retrieve;
    this.serverFields = new Vector<String:>();
       this.serverFields.add(MAILSERVER_FIELD);
 this.serverFields.add(MAILFILE_FIELD);
  // Get users from the database
  UserLoader loader = new UserLoader();
   this.cachedUsers = CacheBuilder.newBuilder().maximumSize(MAX_CACHE_ENTRIES).recordStats()
                                  .expireAfterAccess(EXPIRES_AFTER, UNIT)
          .build(loader);
 }

 public String getDBUrl(Session s, String user) {
  // Crude trick, might fire back at some time
  this.session = s;
  try {
    return this.cachedUsers.get(user);
  } catch (ExecutionException e) {
    // TODO Auto-generated catch block
     e.printStackTrace();
   } return null;
  }
  /**
   * This is the class that actually talks to the directory
  */
   private class UserLoader extends CacheLoaderlt;String>,<String> {
     @Override public String load(String user) throws Exception {
       if (session == null || !session.isValid()) {
        throw new Exception("Session was null or invalid");
         }
    // TODO: verify that this works!
     Vector<String>userVector = new Vector<>(); userVector.add(user); Directory dir = session.getDirectory(); DirectoryNavigator nav = dir.lookupNames(LOOKUP_VIEW_NAME, userVector, serverFields, false); if (!nav.isNameLocated()) { throw new ExecutionException(new Throwable("Can't find user " + user)); } // Now assemble the result StringBuilder result = new StringBuilder(); result.append("notes://"); String serverName = nav.getFirstItemValue().get(0).toString(); String dbName = nav.getNextItemValue().get(0).toString(); Name name = session.createName(serverName); result.append(name.getCommon()); result.append("/"); result.append(dbName.replace("\\", "/")); TaskServlet.shred(name, nav, dir); return result.toString(); } } }

The next piece is the task, which uses a callback to process an individual document:

package com.notessensei.cloudproxy;

import java.io.InputStream;
import java.io.InputStreamReader;

import lotus.domino.Database;
import lotus.domino.Document;
import lotus.domino.NotesException;
import lotus.domino.NotesFactory;
import lotus.domino.NotesThread;
import lotus.domino.Session;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class CloudNotesTask implements Runnable {

 public static CloudNotesTask load(InputStream in) {
  Gson gson = new GsonBuilder().create();
  CloudNotesTask result = gson.fromJson(new InputStreamReader(in), CloudNotesTask.class);
  return result;
 }

 /**
  * Just for testing the tasks
  *
  * @param args
  *            UserName, Action, unid
  */
 public static void main(String args[]) {

  CloudNotesTask it = new CloudNotesTask();
  it.setUserName((args.length < 1) ? "TestUser" : args[0]);
  it.setAction((args.length < 2) ? "DemoTask" : args[1]);
  it.setUnid((args.length < 3) ? "32char-unid" : args[2]);
  System.out.println(it.toJson());

 }

 private String   userName = null;
 private String   action  = null;
 private String   unid  = null;
 private UserCache  userCache = null;
 private IDocumentTask callback = null;

 public final String getAction() {
  return this.action;
 }

 public final String getUnid() {
  return this.unid;
 }

 public final String getUserName() {
  return this.userName;
 }

 public void run() {
  if (!areWeGoodToGo()) {
   return;
  }

  NotesThread.sinitThread();

  try {
   Session s = NotesFactory.createSession();

   String dbURL = this.getDBUrl(s, this.userName);

   if (dbURL == null || dbURL.trim().equals("")) {
    // No database to go to found
    return;
   }

   try {
    Database database = (Database) s.resolve(dbURL);
    Document doc = database.getDocumentByUNID(getUnid());

    this.callback.processDocument(s, doc, this);

    TaskServlet.shred(doc, database);
   } catch (NotesException e) {
    // TODO FIX THE ERROR HANDLING - PLEASE!!!!
    e.printStackTrace();
   }
  } catch (NotesException e1) {
   // TODO FIX THE ERROR HANDLING - PLEASE!!!!
   e1.printStackTrace();
  }
  NotesThread.stermThread();
 }

 public final void setAction(String action) {
  this.action = action;
 }

 /**
  * @param callback
  *            the callback to set
  */
 public final void setCallback(IDocumentTask callback) {
  this.callback = callback;
 }

 public final void setUnid(String unid) {
  this.unid = unid;
 }

 public void setUserCache(UserCache userCache) {
  this.userCache = userCache;
 }

 public final void setUserName(String userName) {
  this.userName = userName;
 }

 public String toJson() {
  GsonBuilder gb = new GsonBuilder();
  gb.setPrettyPrinting();
  gb.disableHtmlEscaping();
  Gson gson = gb.create();
  return gson.toJson(this);
 }

 /**
  * Checks if we have a valid task object and return if we have all we need
  * Does NOT check if the user or the eMail actually exists in the directory
  *
  * @return
  */
 private boolean areWeGoodToGo() {
  return ((this.userName != null) && (this.action != null) && (this.unid != null) && (this.callback != null));
 }

 private String getDBUrl(Session s, String user) {
  if (this.userCache == null) {
   // This should NEVER happen (unless you test it)
   this.userCache = new UserCache();
  }

  return this.userCache.getDBUrl(s, user);
 }

}

Finally the interface and factory that does the actual work on the document:

package com.notessensei.cloudproxy;

/**
 * Provides the implementations to process the documents when triggered from a
 * task
 */
public class DocumentTaskFactory {

 public static IDocumentTask getDocumentTask(CloudNotesTask task) {
  // TODO: Implement Task Classes based on the nature of the task
  System.out.println(task.toJson());
  return null;
 }

}

===============================

package com.notessensei.cloudproxy;
import lotus.domino.Document;
import lotus.domino.Session;

/**
 * Processes a document when triggered from a cloud task
 */
public interface IDocumentTask {

 public void processDocument(Session session, Document document, CloudNotesTask task);

}

As usual: YMMV

Posted by on 17 April 2014 | Comments (1) | categories: IBM Notes

Mustache and CKEditor - Round two


Having just a few static values in the CK Editor drop down list really doesn't cut it. So we extend the bean today to have more flexible options. There are a few that spring to mind:
  1. List of all items in a given document
  2. List of all fields in a form (including subforms), eventually with or without the $ fields
  3. List of provided field names
So here we go:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
 <xp:scriptBlock id="scriptBlock1">
  <xp:this.value><![CDATA[#{javascript:mustache.getFormFields(database, "Memo,Person", false);}]]></xp:this.value>
 </xp:scriptBlock>
 <h1>Mustache and CKEdit demo</h1>
 <xp:inputRichText id="inputRichText1">
  <xp:this.dojoAttributes>
   <xp:dojoAttribute name="extraPlugins" value="mustache">
   </xp:dojoAttribute>  
  </xp:this.dojoAttributes>
 </xp:inputRichText>
</xp:view>

The big change here is the replacement of the EL Expression mustache.sampleData with a SSJS expression, so we can hand over all needed parameters. The pattern is the same for the other variations, so I won't repeat it further onwards. The interesting part is the Java method. Since a form might contain subforms you are interested it, I use an array like sting, that I'll split and a boolean parameter to include system fields. Of course one could vary the approach and automatically figure out the subforms in use (have fun with that once it is conditionally computed) or first present a list of forms and then the respective fields. Also, not to depend on some magic, I add the database dependency as parameter. So you have options to play with.

@SuppressWarnings("unchecked")
public String getFormFields(Database db, String formNameString, boolean includeSystemFields) {
    StringBuilder result = new StringBuilder();
    // Get a sorted set first
    Set<String> fieldNames = new TreeSet<>();
    String[] formNames = formNameString.split(",");
    for (String formName : formNames) {
       try {
            Form form = db.getForm(formName);
            if (form != null) {
                Vector fields = form.getFields();
                for (int i = 0; i < fields.size(); i++) {
                    String curField = fields.get(i).toString();
                    if (includeSystemFields || !curField.startsWith("$")) {
                        fieldNames.add(curField);
                    }
                }
            }
            form.recycle();
        } catch (NotesException e) {
            // Too bad
        }
    }
    result.append(this.preText);
    // Now the content
    for (String f : fieldNames) {
        this.add(result, f);
    }
    result.append(this.postText); return result.toString();
}


Read more

Posted by on 14 April 2014 | Comments (0) | categories: XPages

Lotus de la Mancha


One of my personal heroes is Don Quixote de la Mancha. He is a bat-shit crazy knight, who is true in his courtship of his Lady Dulcinea and never tired to pick a fight with a giant (windmill). His charge against the windmills, is regarded as a result of his craziness, but digging deeper you will find a nobility, worthy of a true knight: stand in for what you deem is right, regardless of the odds of success.
Being true to your calling resonates with me. Wikipedia has an image of the crest of La Mancha.
Based on it I hereby present the coat of arm of Lotus de la Mancha
Lotus de la Mancha - Crest of arms

Posted by on 09 April 2014 | Comments (3) | categories: IBM - Lotus IBM Notes Lotus

CKEditor and Mustache become friends


In the beginning there was WordStar and CSV. The possibility of (then printed) personalized mass-communication had arrived in the form of mail-merge. For Notes eMails that is still a challenge (the latest version of OpenOffice now seems to have a reasonable eMail-Merge, but that's off topic here) since creating the template message with variables to be filled is kind of fuzzy (a.k.a usually out of the reach of mere mortal users).
XPages, Mustache and CKEditor to the rescue! The CKEditor shipping with XPages can be easily customized, so adding a dropdown that inserts Mustache markup shouldn't be too hard. To allow easy reuse of the code, I created a bean than contains all needed components. Add it to a page and simply call the bean functions. The first sample, using only uses static therefore is quite simple:

 
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
 <xp:scriptBlock id="scriptBlock1" value="#{mustache.sampleData}">
 </xp:scriptBlock>
 <h1>Mustache and CKEdit demo</h1>
 <xp:inputRichText id="inputRichText1">
  <xp:this.dojoAttributes>
   <xp:dojoAttribute name="extraPlugins" value="mustache">
   </xp:dojoAttribute>  
  </xp:this.dojoAttributes>
 </xp:inputRichText>
</xp:view>

The variable mustache is configured as a managed bean in faces-config.xml:

<managed-bean>
 <managed-bean-name>mustache</managed-bean-name>
 <managed-bean-class>com.notessensei.xpages.MustacheWrapper</managed-bean-class>
 <managed-bean-scope>session</managed-bean-scope>
</managed-bean>

While it seems a little overkill to move the JavaScript into a Java function, we will see that the bean, once completed, is quite useful and keeps what we need in one place.

Read more

Posted by on 09 April 2014 | Comments (2) | categories: XPages

MongoDB to switch to IBM storage backend


One of the rising stars in NoSQL land is MongoDB. It is prominently featured in IBM BlueMix and in conjunction with Node.js the darling of the startup scene.
However it isn't trouble free, has been called broken by design, bad for data and a folly.
In a bold move to silence all critiques, the makers turned to IBM to get access to a distributed, robust and secure backend storage engine: the venerable NSF. As Bryce Nyeggen clearly stated:" But actually, that’s the Tao-like genius of MongoDB – having absolutely nothing new ", this move fits nicely into the all over strategy. On top of that thousands of XPages developers get instant access to MongoDB's APIs and coolness. Clearly an industry wide win-win!

Posted by on 01 April 2014 | Comments (2) | categories: Software

Communicate with a German


It was going around for a while, how to decode what an English man actually means when he says something. A Harvard Business Review article attributes the insights to Nannette Ripmeester's research and insights. What I was missing in all those tables is the reverse translation. So here it goes:
What a German says What the British should hear What the German meant
Bad idea! Please think about that some more Bad idea! But I will still drink beer with you
This won't work Interesting approach, quite innovative This won't work, but I will still drink beer with you
I like it That's not bad I like it and I want to drink beer with you
What a crap That is interesting What a crap, but I will still drink beer with you
Du bist ein Idiot
(You are an idiot - informal addressing)
With respect... You are not thinking straight today, but I will still drink beer with you
Sie sind ein Idiot
(You are an idiot - formal addressing)
With the greatest respect You are an idiot, go away!
You are insane That is a very brave proposal You are insane, but I will still drink beer with you
This is nonsense Very interesting This is nonsense, but I will still drink beer with you
Shut up and go away! I hear what you say Stop talking, you can't convince me, let's drink beer instead
As usual: YMMV

Posted by on 01 April 2014 | Comments (0) | categories: After hours