function displayServletCode() {
var e = document.getElementById(‘servlet_code’);
e.style.visibility=’visible’;
e.style.display=’block’;
}
When doing a download tracker for binary content there is some issues that must be addressed – one is the MIME type of the file being downloaded. The MIME type determines how the calling browser determines what to do with the response sent by the download tracker. Since the main content being downloaded is going to be images I need to change the MIME type since the browser would otherwise simply display the image. The easy solution is to set the MIME type to application/octet-stream since it will cause the browser to ask where to save the file.
Another obstacle when handling binary data from an agent in Domino is that the default agent output you can obtain from the AgentBase class in Domino is a java.io.PrintWriter. PrintWriter is for character data and therefore not of much use to me.
You obtain the agent output writer by using the getAgentOutput() method of the AgentBase class from which all agents inherit:
import lotus.domino.*;
import java.io.PrintWriter;
public class JavaAgent extends AgentBase {
public void NotesMain() {
try {
Session session = getSession();
AgentContext agentContext = session.getAgentContext();
PrintWriter pw = this.getAgentOutput();
} catch(Exception e) {
e.printStackTrace();
}
}
}
The getAgentOutput()-method is the only method to agent output which is discussed in the Domino 6.5.x help database. As mentioned above a PrintWriter isn’t of much use to me – what I really needed was a way to obtain a java.io.OutputStream implementation. Although not mentioned in the documentation I thought Lotus might have added a way to get a such so I wrote an agent to peek at the available methods as reported by the JVM using reflection:
import lotus.domino.*;
import java.io.PrintWriter;
import java.lang.reflect.*;
public class JavaAgent extends AgentBase {
public void NotesMain() {
try {
Session session = getSession();
AgentContext agentContext = session.getAgentContext();
Method[] methods = this.getClass().getMethods();
for (int i=0; i<methods.length; i++) {
// get info about the method
Class[] param = methods[i].getParameterTypes();
Class rt = methods[i].getReturnType();
Class[] exceptions = methods[i].getExceptionTypes();
int mod = methods[i].getModifiers();
System.out.print(Modifier.toString(mod));
if (rt.equals(Void.class)) {
System.out.print(" void ");
} else {
System.out.print(" " + rt.getName() + " ");
}
System.out.print(methods[i].getName());
System.out.print("(");
for (int j=0; j<param.length; j++) {
if (j>0) System.out.print(", ");
System.out.print(param[j].getName());
}
System.out.print(")");
for (int j=0; j<exceptions.length; j++) {
if (j>0) System.out.print(", ");
System.out.print(exceptions[j].getName());
}
System.out.println(";");
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
As one might have guessed there is a method called getAgentOutputStream() (highlighted in bold) that returns an OutputStream as shown in the snippet below:
public void NotesMain();
public final void startup(lotus.domino.AgentInfo);
public final void runNotes()lotus.domino.NotesException;
public lotus.domino.Session getSession();
public static lotus.domino.Session getAgentSession();
public boolean isRestricted();
public java.io.PrintWriter getAgentOutput();
public java.io.OutputStream getAgentOutputStream();
public void setDebug(boolean);
public void setTrace(boolean);
...
...
Well this was great so I went ahead and coded the actual agent. Getting at the correct document and attachment based on supplied parameters is straight forward using the Domino Java classes. Once I had a hold on the correct attachment as an EmbeddedObject object I used the getInputStream() method to get an InputStream for the attachment. Until this point all is well and good.
From here on it should be quite simple to set the MIME type via the Content-Type HTTP header and read the bytes from the InputStream and writing them to the OutputStream. Or so I thought…
import lotus.domino.*;
import java.io.*;
public class JavaAgent extends AgentBase {
public void NotesMain() {
try {
Session session = getSession();
AgentContext agentContext = session.getAgentContext();
EmbeddedObject o = null;
// code to get at the correct attachment as an EmbeddedObject
// object has been left out
// get output stream and set mime type
OutputStream agent_out = this.getAgentOutputStream();
agent_out.write("Content-Type: application/octet-streamn".getBytes());
// get an input stream for the attachment
InputStream o_in = o.getInputStream();
// write bytes from input stream to output stream
byte[] bytes = new byte[1];
while (o_in.read(bytes) > -1) {
agent_out.write(bytes);
}
// flush and close
agent_out.flush();
agent_out.close();
} catch(Exception e) {
e.printStackTrace();
}
}
}
While the above code compiles and run just fine there is a problem. For some reason I can’t figure out not all the bytes actually reach the browser. Substituting the OutputStream from Domino (agent_out) with a FileOutputStream I can write the attachment to disk just fine so reading and writing is well. The problem must lie with the Domino OutputStream.
Well I’ll save you more ramblings about my debugging attempts but after a LOT of troubleshooting I ended up rewriting the code as a servlet. Once I ported the above code to a servlet it works like a charm when running it from Tomcat (click here to display the code).
Please note: The below code wont compile since the class doing the logging (the LogRequest class) isn’t shown.
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.Iterator;
import java.util.Vector;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lotus.domino.Database;
import lotus.domino.Document;
import lotus.domino.EmbeddedObject;
import lotus.domino.NotesException;
import lotus.domino.NotesFactory;
import lotus.domino.NotesThread;
import lotus.domino.RichTextItem;
import lotus.domino.Session;
import lotus.domino.View;
public class DownloadLogger extends HttpServlet {
// constants
private static final String VIEW_LOOKUP = "0";
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
// declarations
Session session = null;
Database db = null;
View view = null;
Document doc = null;
Attachment a = null;
EmbeddedObject o = null;
InputStream o_in = null;
ServletOutputStream servlet_out = null;
// get initialization parameters
String username = this.getInitParameter("username");
String password = this.getInitParameter("password");
String hostname = this.getInitParameter("hostname");
String server = this.getInitParameter("server");
String dbpath = this.getInitParameter("dbpath");
// get arguments
String unid = req.getParameter("unid");
String file = req.getParameter("file");
try {
// create thread
NotesThread.sinitThread();
// get session
if (null == username || username.length() == 0) {
session = NotesFactory.createSession();
} else {
if (null != hostname && hostname.length() > 0) {
session = NotesFactory.createSession(hostname, username, password);
} else {
session = NotesFactory.createSession((String)null, (String)null, password);
}
}
// get current database and get document
if (null != server && server.length() > 0) {
db = session.getDatabase(server, dbpath);
} else {
db = session.getDatabase(null, dbpath);
}
view = db.getView(VIEW_LOOKUP);
doc = view.getDocumentByKey(unid, true);
if (null == doc) {
// unable to find document
System.out.println("Non-existing document: " + unid);
return;
}
// get the embedded object
a = this.findAttachment(file, doc);
if (null == a) {
// user requested non-existing attachment
System.out.println("Non-existing attachment: " + file);
return;
}
// get servlet out and set content type
res.setContentType("application/octet-stream");
res.setHeader("Content-disposition", "attachment; filename=" + URLEncoder.encode(file));
servlet_out = res.getOutputStream();
// get input stream for attachment
o_in = a.getObject().getInputStream();
byte[] bytes = new byte[1];
while ( (o_in.read(bytes)) > -1 ) {
servlet_out.write(bytes);
}
// close output
servlet_out.flush();
servlet_out.close();
// log succesful download
this.logSuccesfulDownload(session, db, doc, a, req);
} catch (Exception e) {
// log unsuccesful download
try {
this.logUnsuccesfulDownload(session, db, doc, null, req, e);
} catch (Exception e2) {
e2.printStackTrace();
}
} finally {
// terminate thread
NotesThread.stermThread();
}
}
/**
* Utility method for finding an attachment on a document.
*
* @param name The filename.
* @param doc The document to look on.
*/
private Attachment findAttachment(String name, Document doc) throws NotesException {
for (int i=1; i<6; i++) {
RichTextItem rt = (RichTextItem)doc.getFirstItem("Atts" + i);
Vector v = rt.getEmbeddedObjects();
for (Iterator ite=v.iterator(); ite.hasNext(); ) {
EmbeddedObject o = (EmbeddedObject)ite.next();
if (o.getName().equalsIgnoreCase(name) || o.getSource().equalsIgnoreCase(name)) {
Attachment a = this.new Attachment(o, doc.getItemValueString("Description" + i));
return a;
}
}
}
return null;
}
private void logSuccesfulDownload(Session session, Database db, Document doc_archive, Attachment a, HttpServletRequest req) throws Exception {
new LogRequest(session, db).logRequest(session, doc_archive, a, req, null);
}
private void logUnsuccesfulDownload(Session session, Database db, Document doc_archive, Attachment a, HttpServletRequest req, Throwable t) throws Exception {
new LogRequest(session, db).logRequest(session, doc_archive, a, req, t);
}
public class Attachment {
// declarations
private EmbeddedObject pObject = null;
private String pDescription = null;
public Attachment(EmbeddedObject o, String name) {
this.pObject = o;
this.pDescription = name;
}
public String getDescription() {
return pDescription;
}
public EmbeddedObject getObject() {
return pObject;
}
}
}
This just strengthened my observation that the problem was with the Domino OutputStream implementation. Again using reflection I could find out exactly which kind of OutputStream Lotus was using:
...
// get output stream and set mime type
OutputStream agent_out = this.getAgentOutputStream();
System.out.println(agent_out.getClass().getName());
Class parent = agent_out.getClass().getSuperclass();
while (null != parent) {
System.out.println(parent.getName());
parent = parent.getSuperclass();
}
...
The result is a PrintOutputStream as shown by the result below:
java.io.PrintStream
java.io.FilterOutputStream
java.io.OutputStream
java.lang.Object
A java.io.PrintStream will use the default encoding when printing characters so it might be because of this my output data gets corrupted.
Conclusion
Well while the solution using a servlet works it isn’t perfect since it is so much easier to configure an agent than a servlet. Looking at the bright side a servlet provided superiour performance so it isn’t so bad I guess.
To summarize I was happy when I found the getAgentOutputStream() method but was equally disapointed when I found out that it didn’t work. To be fair I don’t know whether it is my code or whether it is something in the obtained OutputStream that teases me. I just hope the above will save someone else from spending precious time fiddeling around with binary data from a Domino web agent and that they will go directly to the servlet.