My next goal after getting the basic
connection to Notes working is to be able to serve
a potential API. Still making friends with the non-blocking approach of
vert.x, I'm taking baby steps forward. In this round I want to be able to deliver a view or folder as JSON string. On a Domino server that is easy. You can use
?ReadViewEntries&OutputFormat=JSON
. On a Notes client you have to do it yourself.
In round one I will ignore categorized views (that's for the next time), but I already will massage the JSON to be leaner. After all why send it over the wire what you don't need. So I have a little
AppConfig.INSTANCE
singleton, that delivers a
viewConfig
object. This object has the list of columns and the inteded labels that I want to be returned.
Since last time some of the libraries have been updated and I'm now running vert.x 3.0.0.Preview1 and the
OpenNTF Domino API RC2. I unpacked the OpenNTF release and removed the Jar files and replaced them with Maven dependencies. This step isn't necessary, but I'm expanding my Maven knowledge, so it was good practise. The starter application looks quite simple:
package com.notessensei.vertx.notes;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.http.HttpServerRequest;
import java.io.IOException;
import org.openntf.domino.thread.DominoExecutor;
public class NotesClient {
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
final NotesClient nc = new NotesClient();
nc.runUntilKeyPresses("q");
System.exit(0);
}
private static final int listenport = 8110;
private static final int dominothreadcount = 10;
private final Vertx vertx;
private final HttpServer hs;
private final DominoExecutor de;
public NotesClient() throws IOException {
this.vertx = Vertx.factory.vertx();
final HttpServerOptions options = HttpServerOptions.options();
options.setPort(NotesClient.listenport);
this.hs = this.vertx.createHttpServer(options);
this.de = new DominoExecutor(NotesClient.dominothreadcount);
}
public void runUntilKeyPresses(String keystring) throws IOException {
int quit = 0;
final int quitKey = keystring.charAt(0);
this.startListening();
while (quit != quitKey) { // Wait for a keypress
System.out.print("Notes Client Verticle started, version ");
System.out.println(AppConfig.INSTANCE.getVersion());
System.out.print("Started to listen on port ");
System.out.println(NotesClient.listenport);
System.out.print("Press ");
System.out.print(keystring);
System.out.println("<Enter> to stop the Notes Client Verticle");
quit = System.in.read();
}
this.stopListening();
System.out.println("\n\nNotes Client Verticle terminated!");
}
private void startListening() {
final Handler<HttpServerRequest> h = new NotesRequestHandler(this.de);
this.hs.requestHandler(h).listen();
}
private void stopListening() {
this.hs.close();
this.de.shutdown();
AppConfig.INSTANCE.save();
}
}
The Notes request handler, checks what is requested and renders the view into JSON using a "homegrown" JSONBuilder which I designed similar to a SAX writer.
package com.notessensei.vertx.notes;
import java.util.Map;
import io.vertx.core.Handler;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import org.openntf.domino.Database;
import org.openntf.domino.Session;
import org.openntf.domino.View;
import org.openntf.domino.ViewEntry;
import org.openntf.domino.ViewNavigator;
import org.openntf.domino.thread.AbstractDominoRunnable;
import org.openntf.domino.thread.DominoExecutor;
import org.openntf.domino.thread.DominoSessionType;
public class NotesRequestHandler extends AbstractDominoRunnable implements Handler<HttpServerRequest> {
private static final long serialVersionUID = 1L;
private transient HttpServerRequest req;
private ViewConfig viewConfig = null;
private final DominoExecutor de;
public NotesRequestHandler(DominoExecutor de) {
this.de = de;
this.setSessionType(DominoSessionType.NATIVE);
}
@Override
public void run() {
Session s = this.getSession();
HttpServerResponse resp = this.req.response();
this.renderInbox(s, resp);
}
public void handle(HttpServerRequest req) {
HttpServerResponse resp = req.response();
String path = req.path();
String[] pathparts = path.split("/");
// The request must have notes in the URL
if (pathparts.length < 3 || !pathparts[1].equals("notes")) {
this.sendEcho(req, resp);
} else {
this.req = req;
// Parameter 3 is either view or inbox
// if it is inbox, we pull in the inbox
if (pathparts[2].equals("inbox")) {
this.viewConfig = AppConfig.INSTANCE.getViewConfig("($Inbox)");
this.de.execute(this);
// if it is view we pull the respective view
} else if (pathparts.length > 3 && pathparts[2].equals("view")) {
this.viewConfig = AppConfig.INSTANCE.getViewConfig(pathparts[3]);
this.de.execute(this);
} /*more here*/ else {
// Nothing value, so we send an check only
this.sendEcho(req, resp);
}
}
}
private void renderInbox(Session s, HttpServerResponse resp) {
resp.headers().set("Content-Type", "application/json; charset=UTF-8");
Database mail = s.getMailDatabase();
resp.end(this.renderView(mail, this.viewConfig));
}
private void sendEcho(HttpServerRequest req, HttpServerResponse resp) {
StringBuilder txt = new StringBuilder();
resp.headers().set("Content-Type", "text/html; charset=UTF-8");
txt.append("<html><body><h1>Notes request handler</h1>");
txt.append("<h2>");
txt.append(req.uri());
txt.append("</h2>");
System.out.println("Got request: " + req.uri());
txt.append("</body></html>");
resp.end(txt.toString());
}
@Override
public boolean shouldStop() {
// TODO Auto-generated method stub
return false;
}
private String renderView(Database db, ViewConfig vc) {
JsonBuilder b = new JsonBuilder();
View view = db.getView(vc.getViewName());
ViewNavigator vn = view.createViewNav();
b.addValue("count", vn.getCount());
b.addValue("name", vc.getViewName());
b.startObject("documents");
for (ViewEntry ve : vn) {
b.startObject(ve.getUniversalID());
b.addValue("position", ve.getPosition());
b.addValue("isRead", ve.getRead());
Map<String, Object> entries = ve.getColumnValuesMap();
for (Map.Entry<String, Object> entry : entries.entrySet()) {
String key = vc.isEmpty() ? entry.getKey() : vc.getColumnName(entry.getKey());
if (key != null) {
b.addValue(key, entry.getValue());
}
}
b.endObject();
}
b.endObject();
return b.toString();
}
}
##
The JSONWriter class needs some more polish to get date formatting right and to keep track of nesting depth, but it should perform well.
What grew quite a bit is the Maven POM model (Maven is to Java roughly what npm is to Node.js or bauer to browser projects):
<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>com.notessensei.vertx</groupId>
<artifactId>com.notessensei.vertx.notes</artifactId>
<version>0.0.2-SNAPSHOT</version>
<name>IBM Notes 9.0 vert.x 3.0 client</name>
<description>Local web</description>
<repositories>
<repository>
<id>sonatype-staging</id>
<url>https://oss.sonatype.org/content/repositories/staging/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<version>3.0.0-preview1</version>
</dependency>
<dependency>
<groupId>com.ibm.icu</groupId>
<artifactId>icu4j</artifactId>
<version>53.1</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.3</version>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-all</artifactId>
<version>5.0.3</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
<dependency>
<groupId>io.reactivex</groupId>
<artifactId>rxjava</artifactId>
<version>1.0.0-rc.3</version>
</dependency>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.1.GA</version>
</dependency>
<dependency>
<groupId>javolution</groupId>
<artifactId>javolution</artifactId>
<version>5.5.1</version>
</dependency>
<dependency>
<groupId>com.ibm.sbt</groupId>
<artifactId>com.ibm.commons</artifactId>
<version>9.0.0</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-hazelcast</artifactId>
<version>3.0.0-preview1</version>
</dependency>
</dependencies>
</project>
Next stops (in no specific order):
- Merge documents from 2 or more views - use case: your mail file and one or more archives
- Render categorized views as nested JSON objects
- Have a HTML UI to look at the list and the individual documents
- Search using Apache Solr
- Folder predictions using AFsNc
Stay tuned!