Search

Twitter

Domino Upgrade

VersionSupport end
5.0
6.0
6.5
7.0
8.0
8.5
Upgrade to 9.x now!
(see the full Lotus lifcyle) To make your upgrade a success use the Upgrade Cheat Sheet.
Contemplating to replace Notes? You have to read this! (also available on Slideshare)

Languages

Other languages on request.

Visitors

Useful Tools

Get Firefox
Use OpenDNS
The support for Windows XP has come to an end . Time to consider an alternative to move on.

About Me

I am the "IBM Collaboration & Productivity Advisor" for IBM Asia Pacific. I'm based in Singapore.
Reach out to me via:
Follow notessensei on Twitter
(posts)
Skype
Sametime
IBM
Facebook
LinkedIn
XING
Amazon Store
Amazon Kindle
NotesSensei's Spreadshirt shop

« Mustache and CKEditor - Round two | Main| Domino Design Pattern: Secret documents »

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	cachedUsers;
	private final Vector				serverFields;
	private Session						session = null;

	public UserCache() {
		// Fields to retrieve;
		this.serverFields = new Vector();
		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 CacheLoader {

		@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 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

Comments

Gravatar Image1 - A domino server without agents is like....an Exchange server?

Disclaimer

This site is in no way affiliated, endorsed, sanctioned, supported, nor enlightened by Lotus Software nor IBM Corporation. I may be an employee, but the opinions, theories, facts, etc. presented here are my own and are in now way given in any official capacity. In short, these are my words and this is my site, not IBM's - and don't even begin to think otherwise. (Disclaimer shamelessly plugged from Rocky Oliver)
© 2003 - 2014 Stephan H. Wissel - some rights reserved as listed here: Creative Commons License
Unless otherwise labeled by its originating author, the content found on this site is made available under the terms of an Attribution/NonCommercial/ShareAlike Creative Commons License, with the exception that no rights are granted -- since they are not mine to grant -- in any logo, graphic design, trademarks or trade names of any type. Code samples and code downloads on this site are, unless otherwise labeled, made available under an Apache 2.0 license. Other license models are available on written request and written confirmation.