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