wissel.net

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

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

Comments

  1. posted by Craig Wiseman on Thursday 17 April 2014 AD:
    A domino server without agents is like....an Exchange server?