Monday, August 18, 2025

HCL Software Pricing Update: 6-9% Increases Effective Today (Aug 18, 2025)

HCL Software just implemented their new renewal pricing policy today with specific percentage increases across their product portfolio. Here's the breakdown:

Pricing Structure:

  • Perpetual License/S&S: 9% increase
  • Term & Other License types: 6% increase

Products Affected (all following above structure):

  • AppScan, BigFix, Commerce
  • Digital Solutions (Domino, Sametime, Leap, Connections, Volt MX Go)
  • Digital Experience (DX), Unica, Volt MX
  • Workload Automation, Secure DevOps & Mainframe products

Key Details:

  • Effective immediately on new software price lists
  • Late renewals will be backdated with additional fees
  • Migration from Perpetual to Term licenses may have promotional offers
  • Multi-year commitments can access additional promotional pricing

Source: HCL Support KB article KB0121452

Thursday, July 10, 2025

XPages Rendering Issue to Be Fixed in Domino 14.5 FP1

If you’ve recently upgraded your Domino server to version 14.5, and you’re working with XPages, you may have encountered a frustrating issue: broken rendering in certain views or components. This bug has been affecting a number of applications, particularly those that rely on partial refresh or dynamically rendered controls.

HCL has officially acknowledged this issue and published a knowledge base article detailing the problem. According to the article (KB0122228), the issue is caused by a regression introduced in recent versions of the XPages runtime, and it affects how JavaScript resources are handled during page rendering.

Good news: a fix is coming soon!

HCL plans to resolve this issue in Domino 14.5 Fix Pack 1 (FP1). If you're running into problems with XPages after upgrading, you’ll want to keep an eye out for this upcoming fix pack.

In the meantime, if your applications are mission-critical and you're stuck with broken UI elements, consider temporarily rolling back to a stable version or using workarounds such as disabling dynamic rendering or tweaking partial refresh logic where possible.

Wednesday, April 02, 2025

Generating PDF Documents in Domino Using PD4ML Java Library

Generating PDF documents from web content or structured data is a common requirement in many applications. When working with HCL Domino, we often need to create PDFs from HTML templates, Notes documents, or dynamically generated content. In this article, I'll walk you through using the PD4ML Java library to generate PDFs within a Domino Java Agent.

Why Use PD4ML?

PD4ML is a powerful Java library that allows you to convert HTML and CSS into high-quality PDF documents. It supports:

  • CSS styling
  • Page breaks and headers/footers
  • Embedded images
  • Table of contents and bookmarks
  • Various output formats (A4, Letter, etc.)

This makes it a great choice for generating invoices, reports, or any structured documents from Domino applications.

Setting Up PD4ML in Domino

To use PD4ML in your Domino environment, follow these steps:

1. Download PD4ML

Get the PD4ML JAR from pd4ml.com. You can use the free or commercial version, depending on your needs.

2. Add PD4ML to Your Domino Project

  1. Place the PD4ML JAR file in the jvm/lib/ext directory of your Domino server (if you want it available for all agents) or within your NSF under WEB-INF/lib (if used in an XPages app).
  2. If using a Java agent, attach the JAR to the agent's Build Path (I usually create a dedicated Java library for external JARs).

3. Write a Java Agent to Generate a PDF

Below is just a snippet to get an idea what you need to do in your Domino Java Agent. The example takes an HTML string and converts it into a PDF:

PD4ML pd4ml = new PD4ML();

String html = "TEST <b>Hello, World!</b>";
ByteArrayInputStream bais = new ByteArrayInputStream(html.getBytes());

// read and parse HTML
pd4ml.readHTML(bais);

File pdf = File.createTempFile("result", ".pdf");
FileOutputStream fos = new FileOutputStream(pdf);

// render and write the result as PDF
pd4ml.writePDF(fos);

PD4ML and HCL Notes/Domino

PD4ML claims they provide support for converting HCL Notes documents into PDFs (I have never checked it though), making it an ideal solution for Domino applications.

Using PD4ML in HCL Domino makes PDF generation straightforward. Whether you need to create reports, invoices, or structured documents, this Java library is a flexible and efficient solution. Try it out in your Domino projects and let me know if you run into any issues!

I have previously used iText and Apache PDFBox for generating PDFs in Domino, as well as various external tools that convert HTML files into PDFs. However, I found PD4ML to be the most user-friendly solution due to its seamless integration with HTML and CSS, built-in support for page formatting, and its ability to handle embedded images and styles with minimal effort.


What tools or libraries do you use in your Domino applications to build PDF files?

Tuesday, September 17, 2024

Implementing SSO Authentication with Saml Using LotusScript & Java

Introduction

SAML (Security Assertion Markup Language) is a widely used protocol for Single Sign-On (SSO), allowing users to authenticate once and access multiple applications. In this article, we'll walk through implementing SAML authentication in a Domino application, detailing the steps needed to provide metadata, make the SSO call, and handle the assertion response (ACS call).

Prerequisites

Before you begin, ensure you have:

  • Access to a SAML Identity Provider (IdP) such as Microsoft Azure AD or Okta.
  • Basic knowledge of Java and HTTP requests.
  • A configured Domino server with the capability to execute Java agents.

Step-by-Step Implementation

1. Providing Metadata

The first step in setting up SAML authentication is to provide metadata that the Identity Provider (IdP) and Service Provider (SP) can use for configuration. You’ll need to supply both IdP and SP metadata to the Domino application.

Basically it means you will need to make 2 public URL: https://yourdomain.com/meta/{id} that provide XML response (and that will be used by IdP) and additionally to that IdP sohuld provide their URL you are going work with.

You also need to find a suitable Java library that can provide your a SamlClient (I use https://github.com/lastpass/saml-sdk-java)

2. Making the SSO Call

Once the metadata is set, you can make the SSO request to the IdP. Here’s the code that generates the SAML authentication request and sends it to the IdP.

String idpURL = config.getItemValueString("IdPURL");
idpMeta = new URL(idpURL).openStream();

String spData = config.getItemValueString("MetaXML");
spMeta = new ByteArrayInputStream(spData.getBytes(StandardCharsets.UTF_8));

// 2. initialize saml client
SAMLInit.initialize();
IdPConfig idpConfig = new IdPConfig(idpMeta);
SPConfig spConfig = new SPConfig(spMeta);
SAMLClient client = new SAMLClient(spConfig, idpConfig);

// 3. send login request
String requestId = SAMLUtils.generateRequestId();
String authrequest = client.generateAuthnRequest(requestId);
String url = client.getIdPConfig().getLoginUrl() + "?SAMLRequest=" + URLEncoder.encode(authrequest, "UTF-8");

The SAMLClient is used to generate the authentication request, and the resulting URL is the login endpoint where the user must be redirected.

3. Handling the Assertion Consumer Service (ACS) Call

After the user is authenticated by the IdP, the SAML response is sent back to the Domino server via the Assertion Consumer Service (ACS) endpoint. The following code handles this response and extracts user attributes from it.

String authresponse = web.getParam("AuthResponse");
String idpURL = config.getItemValueString("IdPURL");
String spData = config.getItemValueString("MetaXML");

SAMLInit.initialize();
IdPConfig idpConfig = new IdPConfig(idpMeta);
SPConfig spConfig = new SPConfig(spMeta);
SAMLClient client = new SAMLClient(spConfig, idpConfig);

AttributeSet aset = client.validateResponse(authresponse);
Map> attr = aset.getAttributes();
for (Map.Entry> entry : attr.entrySet()) {
    String key = entry.getKey();
    List values = entry.getValue();
    doc.replaceItemValue(key, new Vector<>(values));
}

This code validates the SAML response using the SAMLClient, extracts the attributes (such as username, email, etc.), and stores them in the Domino document.

Conclusion

Implementing SAML authentication in Domino involves configuring metadata for the IdP and SP, making an authentication request, and handling the ACS call to process the response. While this example uses the LastPass SAML library, similar methods apply when using other libraries. The overall process is straightforward and can greatly improve the security and convenience of user authentication in your Domino applications.

Monday, September 16, 2024

Implementing SSO Authentication with OpenID Using LotusScript

Introduction

Single Sign-On (SSO) simplifies the user authentication process by allowing users to log in once and gain access to multiple applications. OpenID Connect is an authentication layer on top of OAuth 2.0 that facilitates SSO. In this article, we'll walk through implementing SSO authentication using OpenID Connect with LotusScript.

Prerequisites

Before diving into the code, ensure you have:

  • Basic knowledge of LotusScript and HTTP requests.
  • Familiarity with OpenID Connect and OAuth 2.0.
  • Access to a Microsoft Azure AD tenant or another OpenID Connect provider.

Step-by-Step Implementation

1. Setting Up Your Environment

Make sure your Lotus Domino server is properly configured to handle HTTP requests and that you have access to your OpenID Connect provider's endpoints.

2. Retrieving the Access Token

The access token is obtained after the user successfully authenticates. I am pretty sure outdays most of developer know how to parse values from DocumentContext (fieldsÆ QUERY_STRING or REQUEST_CONTENT). In example below I just use my own class but really you can do it in a few lines if needed

access_token = web.GetRequestParam("access_token")
token_type = web.GetRequestParam("token_type")

3. Making API Requests

Use NotesHTTPRequest to communicate with the OpenID Connect provider’s API. Set the Authorization header with the access token:

Dim session as NotesSession
Dim http As NotesHTTPRequest
Dim jsonNav as NotesJSONNavigator

Set session = new NotesSession
Set http = session.Createhttprequest()
http.Preferjsonnavigator = True
Call http.Setheaderfield("Authorization", token_type & " " & access_token)
Set jsonNav = http.Get("https://graph.microsoft.com/v1.0/me")

4. Parsing the Response

Handle and parse the JSON response to extract user information:

On Error 4843 Resume Next
Dim jsonEl As NotesJSONElement
Dim jsonObj As NotesJSONObject
Set jsonEl = jsonNav.getelementbyname("error")
If Not jsonEl Is Nothing Then
    Set jsonObj = jsonEl.Value
    Print |Status: 401|
    Print "<h2>Error</h2>"
    Print "<p>" & jsonObj.Getelementbyname("code").Value & "</p>"
    Print "<p>" & jsonObj.Getelementbyname("message").Value & "</p>"
    Call scriptLog.LogInfo(jsonNav.Stringify())
    Exit Function
End If

mail = jsonNav.Getelementbyname("mail").Value
displayName = jsonNav.Getelementbyname("displayName").Value

Knowing user's email or other unique data will help you to find a user in your application and make necessary steps for auth.

Conclusion

In my case, I have a web application written in Domino where users can register and sign in without using names.nsf. This approach allows for seamless authentication using OpenID, bypassing the traditional Domino authentication model.

While this solution does not allow users to authenticate directly with Domino, it is still a significant step in that direction. By integrating OpenID Connect, we are moving closer to a more flexible authentication model that can eventually be expanded to support Domino authentication.

Thursday, November 02, 2023

Configuring Entitlement Tracking in Domino 12

In the realm of HCL Domino Server 12.0, the feature of "Entitlement Tracking" has become a vital component for organizations.

While comprehensive information regarding Entitlement Tracking is available through HCL, I needed to know some practical management aspects, such as disabling the feature and adjusting intervals etc.

Disabling Entitlement Tracking:

To disable entitlement tracking, add the following entry and restart the server:

DISABLE_ENTITLEMENT_TRACKING=1

Debugging for Entitlement Tracking Issues:

Debug settings can be incredibly useful for troubleshooting any issues related to Entitlement Tracking. Here's how to configure debugging options:

DEBUG_ENTITLEMENT_AGGREGATOR_INTERVAL=60
DEBUG_UPDATE_ENTITLEMENT_TRACKING=2
ES_OPT_TIMING=1
DEBUG_DIRCAT=3

Hope that will help somebody.

Friday, October 27, 2023

Overcoming Domino's Agent Scheduling Limitations with JavaAddin

If you're frustrated with Domino's limitations on scheduling agents to run more frequently than once every 5 minutes, you're not alone. As a programmer, you understand the need for flexibility and control in your applications. In this article, we'll discuss a practical solution: creating a JavaAddin for Domino that can trigger agents at shorter intervals, allowing you to gain more fine-grained control over your scheduled tasks.

Understanding the Domino Agent Scheduler


Domino provides a robust environment for running scheduled agents. However, it imposes a minimum time gap of 5 minutes between consecutive runs of the same agent. This limitation can be a roadblock for applications that require more frequent execution.


The Power of JavaAddins


JavaAddins offer a way to extend Domino's functionality using Java code. This opens up a world of possibilities, including overcoming the 5-minute scheduling restriction. Here's how you can do it:

1. Setting Up Your JavaAddin

To get started, you'll need to create a JavaAddin. This involves writing Java code to interface with Domino. The code should enable you to trigger agents at shorter intervals than what Domino's native scheduling allows.

2. Utilizing Timers

One of the most effective ways to bypass the 5-minute limitation is to use timers in your JavaAddin. With timers, you can execute your agent at precise intervals, even down to seconds. Here's a simplified example of how this could look in your Java code:
import lotus.domino.*;
public class CustomScheduler extends JavaServerAddin {
    public void runNotes() {
        try {
            Session session = NotesFactory.createSession();
            Database database = session.getDatabase("", "YourDatabase.nsf");
            Agent agent = database.getAgent("YourAgent");

            // Set the execution interval in milliseconds
            int interval = 30000; // 30 seconds

            while (true) {
                agent.runWithDocumentContext(null);
                sleep(interval);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
This code defines a Java thread that runs your specified agent every 30 seconds, effectively bypassing Domino's 5-minute restriction.

3. Deploying Your JavaAddin

Once you've created your JavaAddin, you need to deploy it within your Domino environment. Ensure that the necessary permissions and access controls are in place.

4. Monitoring and Maintenance

Regularly monitor the execution of your custom scheduling solution. Ensure that it's working as expected and doesn't place undue stress on your Domino server.


Conclusion


By creating a JavaAddin for Domino that can trigger agents more frequently, you can take control of your scheduling needs. This solution empowers you to run your agents at shorter intervals, achieving the level of precision your applications require. While this approach requires some development effort, the benefits of fine-grained agent scheduling can greatly enhance your Domino-based applications.

In summary, if you're tired of being constrained by Domino's 5-minute scheduling limitation, consider the power of JavaAddins to break free and gain control over your scheduled agents.

I have also created a bit more advanced setup which you can get on github: DominoAgentsHelper 

Stay tuned for more technical insights, delivered directly to the point, in future articles.

Monday, September 25, 2023

Change Database ReplicaID programmatically

Here is a solution that change ReplicaId of NotesDatabase. Since native capabilities of LotusScript/Java classes do not allow such operation (at least yet), there is a way to do it using C Notes API. Our envrionment consists of both: Windows and Linux servers therefore I had to make a solution that cover both OS.

Declare

Public Const W32_LIB = {nnotes.dll}
Public Const LINUX_LIB = {libnotes.so}

Type TIMEDATE
	Innards(0 to 1) As Long
End Type

Type DBREPLICAINFO
	ID As TIMEDATE			'ID that is same for all replica files
	Flags As Integer		'Replication flags
	CutoffInterval As Integer	'Automatic Replication Cutoff
	Cutoff As TIMEDATE		'Replication cutoff date
End Type

Declare sub W32_OSCurrentTimeDate Lib W32_LIB Alias "OSCurrentTIMEDATE"(Ret As TIMEDATE)
Declare Function W32_NSFDbOpen Lib W32_LIB Alias "NSFDbOpen" (ByVal dbName As String, hdb As Long) As Integer
Declare Function W32_NSFDbClose Lib W32_LIB Alias "NSFDbClose" (ByVal hdb As Long) As Integer
Declare Function W32_NSFDbReplicaInfoGet Lib W32_LIB Alias "NSFDbReplicaInfoGet" (ByVal hdb As Long, hdbr As DBREPLICAINFO) As Integer
Declare Function W32_NSFDbReplicaInfoSet Lib W32_LIB Alias "NSFDbReplicaInfoSet" (ByVal hdb As Long, hdbr As DBREPLICAINFO) As Integer

Declare Sub LINUX_OSCurrentTimeDate Lib LINUX_LIB Alias "OSCurrentTIMEDATE"(Ret As TIMEDATE)
Declare Function LINUX_NSFDbOpen Lib LINUX_LIB Alias "NSFDbOpen" (ByVal dbName As String, hdb As Long) As Integer
Declare Function LINUX_NSFDbClose Lib LINUX_LIB Alias "NSFDbClose" (ByVal hdb As Long) As Integer
Declare Function LINUX_NSFDbReplicaInfoGet Lib LINUX_LIB Alias "NSFDbReplicaInfoGet" (ByVal hdb As Long, hdbr As DBREPLICAINFO) As Integer
Declare Function LINUX_NSFDbReplicaInfoSet Lib LINUX_LIB Alias "NSFDbReplicaInfoSet" (ByVal hdb As Long, hdbr As DBREPLICAINFO) As Integer

Code C API (main part of it)

'GET CURRENT TIMEDATE (TO BUILD NEW REPLICAID): OSCurrentTimeDate
If IS_WINDOWS Then
	Call W32_OSCurrentTimeDate(ReplicaID)
Else
	Call LINUX_OSCurrentTimeDate(ReplicaID)
End If
ReplicaInfo.ID = ReplicaID
	
'SET NEW REPLICAID: NSFDbReplicaInfoSet
If IS_WINDOWS Then
	rc = W32_NSFDbReplicaInfoSet(hDb, replicaInfo)
Else
	rc = LINUX_NSFDbReplicaInfoSet(hDb, replicaInfo)
End If

You can find all solution on GitHub: DominoChangeDatabaseReplicaID

Thursday, September 08, 2022

Java Freemarker with Domino

There are plenty of different Java template engines but for last years I used to stick to FreeMarker. It's open sourced and licensed under the Apache License, Version 2.0.

Here I only want to demonstrate how to integrate it with Domino nicely as it requires to write TemplateLoader class.

Build result based on tempalte "page"
Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
cfg.setDefaultEncoding("UTF-8");
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
cfg.setLogTemplateExceptions(false);

Get the template (uses cache internally)
DominoTemplateLoader dominoLoader = new DominoTemplateLoader(getDatabase());
cfg.setTemplateLoader(dominoLoader);

Template template = cfg.getTemplate("page");

/* Merge data-model with template */
HashMap tags = new HashMap();
tags.put("title", "hellow world");
tags.put("description", "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit.");
			
Writer out = new StringWriter();
template.process(tags, out);
String html = out.toString();
The most important part was actually to build DominoTemplateLoader class and below you can see it
public class DominoTemplateLoader implements TemplateLoader {
	private View m_view;

	public DominoTemplateLoader(Database database) throws NotesException {
		m_view = database.getView("($Template)");
	}

	public void closeTemplateSource(Object templateSource) throws IOException {
		Document doc = (Document) templateSource;
		try {
			doc.recycle();
		} catch (NotesException e) {
			e.printStackTrace();
		}
	}

	public Object findTemplateSource(String id) throws IOException {
		try {
			return m_view.getDocumentByKey(id, true);
		} catch (NotesException e) {
			e.printStackTrace();
		}

		return null;
	}

	public long getLastModified(Object templateSource) {
		Document doc = (Document) templateSource;

		try {
			return doc.getLastModified().toJavaDate().getTime();
		} catch (NotesException e) {
			e.printStackTrace();
		}

		return 0;
	}

	public Reader getReader(Object templateSource, String encoding) throws IOException {
		if (templateSource == null) return null;
		
		Document doc = (Document) templateSource;
		try {
			return doc.getFirstItem("Body").getReader();
		} catch (NotesException e) {
			e.printStackTrace();
		}
		return null;
	}
}
As you can see the Loader class get document form a view and simply get data from item Body.

Friday, December 10, 2021

Journalize email from Exchange to Domino using Addin

One of my customer moves to Office 365 and also wants to move to to Exchange/Outlook 356 during next year while keeping Domino app running as it is of now.

They have customized mail boxes with few actions which allow to journalize emails into their Domino applications and that is quite critical functionality so that would need to be mirrored.

We have decided to built Outlook Add-in (works in web, client and also with mobile devices).

Here are a few advises to those who would need to developer similar functionality.

1. Create a outlook add-in project and define manifest

You would have to create a project with manifest and needed html, css, js elementets. You can easily find information how to do that on MS sites (not going to provide any links as they could change in future). That will allow you to define UI, see my example.


2. Send memo ID to Domino

We need to get information about email from Outlook Add in and send those items to Domino (you would have to build REST API on Domino side that can receive data from Outlook).

Office.context.mailbox.getCallbackTokenAsync(function(result) {

  ...

  var token = result.value;
  var ewsurl = Office.context.mailbox.restUrl;
  var ewsItemId = Office.context.mailbox.item.itemId;
  const itemId = Office.context.mailbox.convertToRestId(ewsItemId,Office.MailboxEnums.RestVersion.v2_0);

  // send token, ewsurl and itemId to Domino endpoint
  ...

}

Having those keys (token url and itemId) you can pull email in Mime format

3. Convert Mime to Notes email

So at this point Domino received data from Add in and can finally do another request to Exchange server (using token, ewsurl and itemId) to read the memo MIME

Dim http As NotesHTTPRequest
Dim enpoint As string
		
Set http = m_app.NotesSession.Createhttprequest()
Call http.Setheaderfield("Authorization", "Bearer " + token)
enpoint = ewsurl + |/v2.0/me/messages/| + itemId + |/$value|
		
getItemMIME = http.get(enpoint)

There is no native Domino LS/Java Mime Parser however I found working example by Stephan: Importing EML files into Notes (lots of them). It worked well, but seems it does not handle inline images (need to do more testing etc).

Alternatively I was told that there is MimeMessageParser.java that writes MIME to a Notes document. This class is part of the XPages Extension Library. So it has sense to compare them.

Monday, November 22, 2021

Alter user during authentication using DSAPI

I had a need to alter user during web-authentication process on fly (skipping password validation). Initially the task looked impossible but I managed to solve it using DSAPI filter.
Though the solution looks quite unsecure it could be very useful in some cases (by high level administrators) who needs to 'signin' as a user in their organization to do some checks.

Here are few most important snippets how to do that:

1. Subscribe for the event kFilterAuthenticate

That means that our dsapi filter only intercepts one specific event: kFilterAuthenticate), as there are other 10-15 other events which we do not wanna touch.

EXPORT unsigned int FilterInit(FilterInitData* filterInitData) {
	STATUS   error = NOERROR;

	filterInitData->appFilterVersion = kInterfaceVersion;
	filterInitData->eventFlags = kFilterAuthenticate;

	// other logic
	// ...
}

2. Catch the authenticate event and process it

Get our event and associate it with a C function

EXPORT unsigned int HttpFilterProc(FilterContext* context, unsigned int eventType, void* eventData) {
	/* Include only those events we want to handle */
	switch (eventType) {
	case kFilterAuthenticate:
		return Authenticate(context, (FilterAuthenticate *) eventData);
	default:
		break;
	}

   return kFilterNotHandled;
}	// end HttpFilterProc

3. Finally set a desired username

Below I only show the key moment - replace user name with another name

unsigned int Authenticate(FilterContext* context, FilterAuthenticate* authData) {
	/* logic that calculate username  */
    // .................................
    // char[] fullName = "CN=T5 Tester5/O=DmytroDev";
    // .................................

	/* Copy the canonical name for this user that dsapi requires.  */
	strncpy ((char *)authData->authName, fullName, authData->authNameSize);
	authData->authNameSize = strlen(alterAuthToken);
	authData->authType = kAuthenticBasic;
	authData->foundInCache = TRUE;

	return kFilterHandledEvent;
}

In order to improve security I have built an application on Domino side that generates tokens which have to be set in cookie and then DSAPI filter reads the cookie and get username from database. Tokens could be generated only by certain people are will be deleted by schedule agents after some time.



On the screenshot below you can see that I signed in as a "T5 Tester5" using my custom token AlterAuthToken while I am Anonymous.



Friday, November 12, 2021

Clear database replication history programatically

Recently I had a need to make a solution that can periodically clean replication history for list of databases.

Native LotusScript/Java classes do not allow that, but there is an C API for that.

Here is a cross platform solution (works for Windows/Linux)

Declare

Public Const W32_LIB = {nnotes.dll}
Public Const LINUX_LIB = {libnotes.so}

Declare Function W32_NSFDbOpen Lib W32_LIB Alias {NSFDbOpen} (ByVal dbName As String, hDb As Long) As Integer
Declare Function W32_NSFDbClose Lib W32_LIB Alias {NSFDbClose} (ByVal hDb As Long) As Integer
Declare Function W32_NSFDbClearReplHistory Lib W32_LIB Alias {NSFDbClearReplHistory} (ByVal hDb As Long, flags As Integer) As Integer

Declare Function LINUX_NSFDbOpen Lib LINUX_LIB Alias {NSFDbOpen} (ByVal dbName As String, hDb As Long) As Integer
Declare Function LINUX_NSFDbClose Lib LINUX_LIB Alias {NSFDbClose} (ByVal hDb As Long) As Integer
Declare Function LINUX_NSFDbClearReplHistory Lib LINUX_LIB Alias {NSFDbClearReplHistory} (ByVal hDb As Long, flags As Integer) As Integer

Using C API functions

// get a handler to database
If IS_WINDOWS Then
	rc = W32_NSFDbOpen(Server & "!!" & FileName, hDb)
Else
	rc = LINUX_NSFDbOpen(Server & "!!" & FileName, hDb)
End If

// clear replication history
If IS_WINDOWS Then
	rc = W32_NSFDbClearReplHistory(hDb, 0)
Else
	rc = LINUX_NSFDbClearReplHistory(hDb, 0)
End If

// close datababase (be sure you always close hDb if you opened it, otherwise memory leak).
If IS_WINDOWS Then
	rc = W32_NSFDbClose(hDb)
Else
	rc = LINUX_NSFDbClose(hDb)
End If

Be sure that you always close hDb handler if you opened the database, otherwise it would lead to memory leak)

See the full solution on GitHub: DominoReplicationHistoryCleaner

Thursday, April 08, 2021

Checking if database is encrypted with LotusScript (C API)

Since it's not possible to identify encryption status and level using native LotusScript/Java classes here is a way to do that. The solution is based on Notes CAPI (within LotusScript) but it works for both Linux/Windows environment.

I will omit NSFDbOpen and NSFDbClose since it's easy to find out and focus instead on the main function: NSFDbLocalSecInfoGetLocal.

Declaration

Const NNOTES ="nnotes.dll"
Const LIBNOTES ="libnotes.so"

Declare Public Function WIN_NSFDbLocalSecInfoGetLocal Lib NNOTES Alias "NSFDbLocalSecInfoGetLocal"(ByVal hDb As Long, state As Long, strength As Long) As Integer
Declare Public Function LIN_NSFDbLocalSecInfoGetLocal Lib LIBNOTES Alias "NSFDbLocalSecInfoGetLocal"(ByVal hDb As Long, state As Long, strength As Long) As integer

Function check encryption status

public Function NSFDbLocalSecInfoGetLocal(hDB As Long, state As Long, strength As long) As Integer
 If isDefined("WINDOWS") Then
  NSFDbLocalSecInfoGetLocal = WIN_NSFDbLocalSecInfoGetLocal(hDb, state, strength)
 ElseIf isDefined("LINUX") Then
  NSFDbLocalSecInfoGetLocal = LIN_NSFDbLocalSecInfoGetLocal(hDb, state, strength)
 End If
End Function

Example how to use it

Private Function calcEncryption(database As NotesDatabase, doc As notesdocument)
 Dim sDb As String
 Dim hDb As Long
 Dim state As Long
 Dim encrypt As Long
 Dim rc As Integer

 sDb = database.server & "!!" & database.filepath

 rc = NSFDbOpen(sDb, hDb)
 If rc <> 0 Then Exit function

 rc = NSFDbLocalSecInfoGetLocal(hDB, state, encrypt)
 If rc <> 0 Then
  Error 9001, "Impossible to read encryption. Error code: " & CStr(rc)
 End If

 rc = NSFDbClose(hDb)
End Function
  • state: 0 (not encrypted), 1 (encrypted) or 2 (will be encrypted after compact)
  • encrypt: 1 (easy), 2 (middle), 3 (strong)

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