Wednesday, January 20, 2021

How to post attachments using form to agent

I have a form with some text fields and I also needed to send attachments within same form.

Form is printed by agent and is processed by another agent written in LotusScript.

I spent some time working on solution and here it is.

The idea is to convert selected files to base64 on client side and then post data on submission and agent that process submission will conver base64 to file.

Here is a form, note that we run some logic when files are added

<form name="formName" method="post" action="agentName?openagent">
<input name="title" value="xxx">
<input type="file" name="files" multiple onchange="toBase64()">
</form>

Here is how we convert selected files to base64 and how we results as text fields to form (JS is not optimal, it can be done without jQuery)

function toBase64() {
  var files = document.querySelector('input[type=file]').files;

  var form = $("form");
  form.find("input[name^='filebase64']").remove(); // replace

  function readAndSave(file, index) {
    var reader = new FileReader();
	
    reader.addEventListener("load", function() {
      form.append("<input name="filebase64_"+index + "" type="hidden" value=""+this.result+"" />");
      form.append("<input name="filename_"+index + "" type="hidden" value=""+file.name+"" />");
    }, false);

    reader.readAsDataURL(file);
  }

  if (files) {
    [].forEach.call(files, readAndSave);
  }
}

Once form is submitted we have to read base64 items and convert them to file. There are at least 2 solutions: pure LS or Java/LS2J

a) LotusScript using NotesMIMEHeader
Private Function saveBase64AsFile(base64 As String, filePath As string) As Boolean
	On Error GoTo ErrorHandler

	Dim stub As NotesDocument
	Dim stream As NotesStream
	Dim item As NotesMIMEEntity
	Dim header As NotesMIMEHeader
	Dim emb As NotesEmbeddedObject
	Dim fileName As String
	Dim contentType As string
	Dim base64File As String

	fileName = StrRightBack(filePath, "\")
	contentType = StrRight(Strleft(base64, ";"), ":")
	base64File = StrRight(Base64, ",")
	
	Call scriptLog.loginfo(fileName)
	Call scriptLog.loginfo(contentType)
	
	Set stub = db.Createdocument()
	Set item = stub.CreateMIMEEntity("Body")
	Set header = item.createHeader("Content-Disposition")
	Call header.setHeaderVal({attachment; filename="} & fileName & {"})

	Set stream = app.NotesSession.CreateStream()
	Call stream.WriteText(base64File)
	Call item.SetContentFromText(stream, contentType, ENC_BASE64)

	Call stream.Truncate
	Call stream.Close
	Call stub.Closemimeentities(True)

	Set emb = stub.Getattachment(fileName)
	Call emb.Extractfile(filePath)
	
	Exit Function
ErrorHandler:
	Error Err, Error
End Function
b) Java with LS2J using native classses.
import java.util.Base64;
import java.io.IOException;
import java.nio.file.*;

public class Base64ToFile{

	public boolean convert(String base64String, String filePath) {
		try {
			byte[] decodedImg = Base64.getDecoder().decode(base64String.getBytes());
			Path destinationFile = Paths.get(filePath);
			Files.write(destinationFile, decodedImg);
			return true;
		} catch (IOException e) {
			e.printStackTrace();
		}
		return false;
	}
}
UseLSX "*javacon"
Use "Base64ToFile"

Class Base64ToFile
	Private jSession As JavaSession
	Private jClass As Javaclass
	Private jObject As JavaObject
	Private jError As JavaError
	
	Sub New()
		Set jSession = New JavaSession
		Set jClass = jSession.GetClass("Base64ToFile")
		Set jObject = jClass.Createobject()
	End Sub
	
	Public Function convert(base64 As String, filePath As String) As Boolean
		convert = jObject.convert(base64, filePath)
	End Function
End Class

Tuesday, October 20, 2020

JavaServerAddin in Domino - constructor and schedule

In this article we will improve our java addin 'DemoAddin' with following features:

  1. Constructor that accepts parameters when we load java addin from console.
  2. We will schedule output to console amount of registered person in names.nsf (every 33 seconds).
  3. Define destructor.
  4. Load (with parameter) and Unload addin from console

Constructor

We will define 2 constructor, one that can accept parameters and one in case if we load addin without any parameters. It is pretty obvious how it works.

// we expect our first parameter is dedicated for secondsElapsed
public DemoAddin(String[] args) {
	this.secondsElapsed = Integer.parseInt(args[0]);
}

// constructor if no parameters
public DemoAddin() {}

Means if we run command like below it will run using constructor with parameters

load runjava org.demo.DemoAddin 33

Schedule worker

There is a method (it works but deprecated, however I have not found what should be use instead). The snippet below run constant loop, open names.nsf, read amount of users in the view People and output it to console. The main line here is this.addInRunning(), it keeps loop running forever (until we change it to this.stopAddin() or unload addin from console)

session = NotesFactory.createSession();
String server = session.getServerName();

while (this.addInRunning()) {
	/* gives control to other task in non preemptive os*/
	OSPreemptOccasionally();

	if (this.AddInHasSecondsElapsed(secondsElapsed)) {
		ab = session.getDatabase(server, "names.nsf");
		long count = ab.getView("People").getAllEntries().getCount();
		logMessage("Count of persons: " + Long.toString(count));
		ab.recycle();
	}
}

Destructor

Keep in mind that we need to be careful with Notes object, we have to release memory (recycle) after we no longer use them. So it's a good idea to create own terminate method that release memory for all Notes object you delcared and use it when addin unloads. There is also built-in method finalize so you can put code there, but I prefer to have own method and use it in places I need

private void terminate() {
	try {
		if (this.ab != null) {
			this.ab.recycle();
		}
		if (this.session != null) {
			this.session.recycle();
		}

		logMessage("UNLOADED (OK)");
	} catch (NotesException e) {
		logMessage("UNLOADED (**FAILED**)");
	}
}

Load (with parameter) and Unload addin from console

In order to run addin with parameter simply add it after name of Addin, use space as a separator when you need more than 1 parameter

load runjava org.demo.DemoAddin 33
[1098:0002-23A0] 10/20/2020 11:15:00 AM  JVM: Java Virtual Machine initialized.
[1098:0002-23A0] 10/20/2020 11:15:00 AM  RunJava: Started org/demo/DemoAddin Java task.
[1098:0004-3984] 10/20/2020 11:15:00 AM  DemoAddin: version             2
[1098:0004-3984] 10/20/2020 11:15:00 AM  DemoAddin: build date          2020-10-19 11:00 CET
[1098:0004-3984] 10/20/2020 11:15:00 AM  DemoAddin: java                1.8
[1098:0004-3984] 10/20/2020 11:15:00 AM  DemoAddin: seconds elapsed     33
[1098:0004-3984] 10/20/2020 11:15:33 AM  DemoAddin: Count of persons: 11
[1098:0004-3984] 10/20/2020 11:16:06 AM  DemoAddin: Count of persons: 11
[1098:0004-3984] 10/20/2020 11:16:39 AM  DemoAddin: Count of persons: 11
[1098:0004-3984] 10/20/2020 11:17:12 AM  DemoAddin: Count of persons: 11

When you want to unload addin using console here is a command

tell runjava unload org.demo.DemoAddin

And you should be see confirmation on console if everything went fine

[1098:0004-3984] 10/20/2020 11:26:55 AM  DemoAddin: UNLOADED (OK)
[1098:0002-23A0] 10/20/2020 11:26:55 AM  RunJava: Finalized org/demo/DemoAddin Java task.
[1098:0002-23A0] 10/20/2020 11:26:56 AM  RunJava shutdown.

If you are interested in this topic I can recommend at least two more sources NSFTools.com JavaAddinTest and AndyBrunner / Domino-JAddin or wait for new articles in my blog :-). Also feel free to ask questions if you are uncertain.

Full version of DemoAddin class is hosted on github: DominoDemoAddin

All articles in series
  1. JavaServerAddin in Domino - introduction
  2. JavaServerAddin in Domino - constructor and schedule

Wednesday, October 14, 2020

JavaServerAddin in Domino - introduction

I will show how to build, register and load simple JavaAddin for Domino. I'm not entirely sure if lotus.notes.addins.JavaServerAddin is supported by HCL, so use that for your own sake.

1) Java class

import lotus.notes.addins.JavaServerAddin;

public class DemoAddin extends JavaServerAddin {
	public void runNotes() {
		AddInLogMessageText("Hello world", 0);
	}
}

2) JAR - from project

Export/build JAR file from the DemoAddin project (we are going to put jar file in the Domino folder).

3) Register JavaAddin

Place JAR file under Domino, f.x. path could be (DemoAddin is a folder and it could be just any name, DemoAddin-1.jar is our JAR file we built earlier)

C:\IBM\Domino\DemoAddin\DemoAddin-1.jar

and then register it in server's notes.ini using variable JAVAUSERCLASSES. In case if there are other addin registered there use semicolon as a separator for Windows and a colon for Linux

JAVAUSERCLASSES=addin-1;.\DemoAddin\DemoAddin-1.jar;addin-2

Alternatively simply put JAR file into the folder \jvm\lib\ext, but personally I prefer to keep customization separately instead of mixing core JAR files with customization. Additionally I'm not sure what happens to custom JAR file when is upgradet.

4) Load JavaAddin

It's time to run our DemoAddin. From console run following command

load runjava DemoAddin

Take into account if your include your class into a package, f.x. package org.demo; than you should add that into run command

load runjava org.demo.DemoAddin

If everything went fine you should see 3 lines

RunJava: Started DemoAddin Java task.
Hello world
RunJava: Finalized DemoAddin Java task.

Possible error

If you registered JAR file incorrectly, the error could be like this. In such case just make sure you followed steps properly.

RunJava: Can't find class DemoAddin1 or lotus/notes/addins/demoaddin1/DemoAddin1 in the classpath.  Class names are case-sensitive.

If I find time, I will make few more posts about this topic. It's really just a tip of the iceberg.

All articles in series
  1. JavaServerAddin in Domino - introduction
  2. JavaServerAddin in Domino - constructor and schedule

Thursday, February 27, 2020

NotesRichText to HTML native within Domino 10

Just realized that Domino 10+ came with possibility to convert RichTextItem to HTML almost in 1 line.

RichTextItem rt = (RichTextItem) doc.getFirstItem("Body");
String html = rt.convertToHTML(null);

Finally all these tricky transformation of RichText to HTML can be removed, same goes to custom JSON and HTTPRequest libraries.

I wonder what other useful improvements I missed?

Monday, February 17, 2020

Rewrite URL with CloudFlare for Domino

Years ago I created few solutions for Domino using DSAPI:
  1. remove last slash in URL served by Domino.
  2. rewrite URL.
  3. better control over 404/500 error pages.

It was quite complicated solution (DSAPI is not easy topic).
Today another client asked similar features (remove last slash and rewrite url).
I started to recall how DSAPI works but then I reminded myself that the client stick with CloudFlare in front of their Domino servers.

Cloudflare has 'page rules' which allow to solve issue with last trailing slash. Just matter of configuration.

And about rewriting URL it's actually possible to achieve with workers! You can see below how to rewrite URL.
In example below I changed url like
domain.com/section/page?param1=aaa&param2=bbb
=>
domain.com/router?openagent&req=section/page&param1=aaa&param2=bbb

addEventListener('fetch', event => {
  const url = new URL(event.request.url);
  const pathname = url.pathname.substr(1);
  if (pathname.startsWith("design") || pathname.startsWith("files") || pathname.startsWith("api")) {
    return;
  }
  event.respondWith(handleRequest(event.request));
})

/**
 * Rewrite URL and makes query param available
 * @param {Request} request
 */
async function handleRequest(request) {
  let url = new URL(request.url);
  let pathname = url.pathname.substr(1);
  url.pathname = "?openagent&req="+pathname;
  var query = url.search;
  if (query!="") {
    url.pathname += "&" + query.substr(1);
  }

  const newRequest = new Request(url, new Request(request));
  return await fetch(newRequest)
}

Wednesday, September 25, 2019

JSON Reader in LotusScript

For those who are still running Domino v9 (or below) here is a LotusScript library that can parse JSON into something useful. I implemented it some time ago and since that time almost had no issues with it. Since many customers that did not migrate to v10 (and probably won't do that in near future) it could be very useful to them.

You can download my realization on github: jsonparser-ls

See example below how it works

Dim parser As JSONParser
Dim jsonObj As JSONObject
Dim jsonArr As JSONArray
Dim jsonString As String

Set parser = New JSONParser

'object
jsonString = |{"array":[1,  2  ,   300.56  ]  ,  "boolean":true,"null":null,"number":123,"object":{"a":"b","c":"d","arr":["12","23",34.56],"e":"f","ho":true},"string":"Hello World"}|
Set jsonObj = parser.parse(jsonString)
'test
Print jsonObj.HasItem("array") 'true
Print jsonObj.HasItem("array1") 'false
print jsonObj.GetItem("array").Items(2) '300.56
print IsNull(jsonObj.GetItem("null")) 'true
print jsonObj.GetItem("number") '123
print jsonObj.GetItem("object").getItem("c") 'd
print jsonObj.GetItem("object").getItem("ho") 'true
print jsonObj.GetItem("object").getItem("arr").Items(2) '34.56

'array
jsonString = |[{a:1,b:true,_dd:null},12,"13",true,{}]|
Set jsonArr = parser.parse(jsonString)
'test
print jsonArr.Items(0).getItem("b") 'true
print jsonArr.Items(1) '12
print jsonArr.Items(2) '13
print jsonArr.Items(3) 'true
print TypeName(jsonArr.Items(4)) '"JSONOBJECT"

Tuesday, September 24, 2019

Create Excel files with LotusScript without Excel installed

One of my customer asked me to find a solution to create Excel files using LotusScript on server without Excel on it (well who wants to do install Excel and other tools on Server). Took some time but I have made a proof of concept using Apache POI and it worked very very nice. I have also made a LS2J cover so it's more easily for people who are not familiar with Java to create Excel files.

I put demo on my github account with some explanation so feel free to have a look on it: excel-apache-ls but if you wonder how it works, see snippet below:

Option Public
Option Declare

UseLSX "*javacon"
Use "Apache.Excel"

Sub Initialize
 Dim jSession As JavaSession
 Dim jClass As Javaclass
 Dim jObject As JavaObject
 Dim filepath As String
 Dim row As Integer

 Set jSession = New Javasession
 Set jClass = jSession.GetClass("explicants.office.Excel")
 Set jObject = jClass.Createobject()
 
 Call jObject.createSheet("sheet A-100")
 Call jObject.createSheet("sheet B-100")
 Call jObject.createSheet("sheet C-100")
 
 Call jObject.getSheet("sheet A-100")

 row = row + 1
 Call jObject.setCellValueString("lorem", row, 0)
 Call jObject.setCellValueString("ipsum", row, 1)
 Call jObject.setCellValueDouble(55, row, 2)
 
 row = row + 1
 Call jObject.setCellValueString("hello", row, 0)
 Call jObject.setCellValueString("world", row, 1)
 Call jObject.setCellValueDouble(200.50, row, 2)
 
 row = row + 1
 Call jObject.setCellValueString("gurli gris", row, 0)
 Call jObject.setCellValueString("george", row, 1)
 Call jObject.setCellValueDouble(0.505, row, 2)
 
 filepath = Environ("Temp") & Join(Evaluate({@Unique})) & ".xls"
 Call jObject.saveAsFile(filepath)
 
 MsgBox filepath
End Sub

Tuesday, September 03, 2019

Formatting NotesDatetime as Java SimpleDateFormat

Recently I had to made proper formatting of NotesDateTime object (using LotusScript) with respect to different locale. I had written a cover in LS2J for Java SimpleDateFormat class that works independently (without Java library), so copy/paste and go on.

Here is an example how it works
Dim jdtr As New jDateTimeRich
Dim dt As New NotesDateTime("15-10-2017 10:20:30")

MsgBox jdtr.SimpleDateFormat(dt, "dd-MM-yyyy", "", "") ' "15-10-2017"
MsgBox jdtr.SimpleDateFormat(dt, "d-MMM-yy", "ru", "RU") ' "15-окт-17"
MsgBox jdtr.SimpleDateFormat(dt, "EEEEE MMMMM yyyy HH:mm:ss.SSSZ", "da", "DK") ' "søndag oktober 2017 11:20:30.000+0200")

The beautiful part of it - is translation of months and days according to Locale.
I have uploaded source/class on github. Feel free to re-use it: jDatetimeRich-LS and report issues of course.

Tuesday, July 17, 2018

Domino with Java 6 and TSL 1.2

Recently I have faced an issue where one of our provider changed SSL and they disabled supporting of TLS 1.0 (as far as I understand it's non secure ourdays) and TLS 1.2 should be used instead. As a result our java agents (which used HttpsURLConnection) could not connect anymore to provider.

Error message looked like this:
Caused by: java.security.AccessControlException: Access denied (javax.net.ssl.SSLPermission setHostnameVerifier)
I have found 2 possible solutions:

Enable TLS 1.2 on Domino (applicable only for 9.0.1 FP3 IF2 and higher)


The Domino JVM is based on Java 1.6 and default settings configured in a way to use TLS 1.0. Luckily our Domino servers had version 9.0.1 FP4 (and TSL 1.2 support has been added since FP3 IF2). So our version was capable to work with 1.2 (in theory) but it took some time to make it work.

In order to configure Domino JVM to use TLS 1.2 you need to:
  1. Create JVM settings file, f.x. C:\Domino\jvmOptions.ini
  2. Add parameter in jvmOptions.ini
    https.protocols=TLSv1.2
  3. Add path to jvmOptions.ini file in notes.ini
    JavaUserOptionsFile=C:\Domino\jvmOptions.ini
After you added settings don't forget to restart Domino server. Keep in mind that setting is global meaning all agents that will start to use TLS1.2 therefore it is definitely worth to verify everything before and after this fix.

Java library solution


If that is not a way you can go with (f.x. Domino has lower version or something won'f work if you switch to TLS 1.2) then it's still possible to make custom Java Library that will make it possible, see link: How to use TLS 1.2 in Java 6.

It worked for me as well, but it requires to give permission in java policy on Domino server.