Code – Read from and Write to Windows Registry in Lotusscript

A question was posted in the IBM DeveloperWorks forum for Notes/Domino 8 about the possibility to detect from within Notes if a computer is equipped with a touch screen. The answer was that you have to check if a specific DLL is installed, which is done though the registry. The original posted then asked how to do that in Lotusscript, so I deceded to simply post some code I am using. I did not write this code, and I don't know who originally did. I think I may have taken some VB code and simply adapted it for Lotusscript. I plan to rewrite this a s a class when I have some time. In the mean time, here is the code.   Option Public Option Declare Dim REG_NONE As Long Dim REG_SZ As Long Dim REG_EXPAND_SZ As Long Dim REG_BINARY As Long Dim REG_DWORD As Long Dim REG_DWORD_LITTLE_ENDIAN As Long Dim REG_DWORD_BIG_ENDIAN As Long Dim REG_LINK As Long Dim REG_MULTI_SZ As Long Dim REG_RESOURCE_LIST As Long Dim REG_FULL_RESOURCE_DESCRIPTOR As Long Declare Function RegCloseKey Lib "advapi32.dll" (Byval hKey As Long) As Long Declare Function RegCreateKeyEx Lib "advapi32.dll" Alias "RegCreateKeyExA" (Byval hKey As Long, _ Byval lpSubKey As String, Byval Reserved As Long, Byval lpClass As String, _ Byval dwOptions As Long, Byval samDesired As Long, Byval lpSecurityAttributes As Long, _ phkResult As Long, lpdwDisposition As Long) As Long Declare Function RegOpenKeyEx Lib "advapi32.dll" Alias "RegOpenKeyExA" (Byval hKey As Long, _ Byval lpSubKey As String, Byval ulOptions As Long, Byval samDesired As Long, _ phkResult As Long) As Long Declare Function RegSetValueExString Lib "advapi32.dll" Alias "RegSetValueExA" (Byval hKey As Long, _ Byval lpValueName As String, Byval Reserved As Long, Byval dwType As Long, Byval lpValue As String, _ Byval cbData As Long) As Long Declare Function RegSetValueExLong Lib "advapi32.dll" Alias "RegSetValueExA" (Byval hKey As Long, _ Byval lpValueName As String, Byval Reserved As Long, Byval dwType As Long, lpValue As Long, _ Byval cbData As Long) As Long Declare Function RegQueryValueExString Lib "advapi32.dll" Alias "RegQueryValueExA" _ (Byval hKey As Long, Byval lpValueName As String, Byval lpReserved As Long, lpType As Long, _ Byval lpData As String, lpcbData As Long) As Long Declare Function RegQueryValueExLong Lib "advapi32.dll" Alias "RegQueryValueExA" _ (Byval hKey As Long, Byval lpValueName As String, Byval lpReserved As Long, lpType As Long, _ lpData As Long, lpcbData As Long) As Long Declare Function RegQueryValueExNULL Lib "advapi32.dll" Alias "RegQueryValueExA" _ (Byval hKey As Long, Byval lpValueName As String, Byval lpReserved As Long, lpType As Long, _ Byval lpData As Long, lpcbData As Long) As Long ' --- Registry key values Const HKEY_CLASSES_ROOT = &H80000000 Const HKEY_CURRENT_USER = &H80000001 Const HKEY_LOCAL_MACHINE = &H80000002 Const HKEY_USERS = &H80000003 Const HKEY_CURRENT_CONFIG = &H80000005 ' --- Registry return values Const ERROR_NONE = 0 Const ERROR_BADDB = 1 Const ERROR_BADKEY = 2 Const ERROR_CANTOPEN = 3 Const ERROR_CANTREAD = 4 Const ERROR_CANTWRITE = 5 Const ERROR_OUTOFMEMORY = 6 Const ERROR_INVALID_PARAMETER = 7 Const ERROR_ACCESS_DENIED = 8 Const ERROR_INVALID_PARAMETERS = 87 Const ERROR_NO_MORE_ITEMS = 259 ' --- Registry access key Const…

3 Comments

Free Code – Class to read URL name-value pairs

Here is another little code snippet I want to share. I use it all the time in my Lotusscript-based Domino web agents, and I figured that other could benefit from it as well. It is just an easy way to check for and read the name-value pairs (arguments) passed from the browser to the web server by HTTP GET or POST calls. Put the code below in a script library, I call it Class.URL: %REM Library Class.URL Created Oct 9, 2014 by Karl-Henry Martinsson Description: Lotusscript class to handle incoming URL (GET/POST). %END REM Option Public Option Declare %REM Class URLData Description: Class to handle URL data passed to web agent %END REM Class URLData p_urldata List As String %REM Sub New() Description: Create new instance of URL object from NotesDocument %END REM Public Sub New() Dim session As New NotesSession Dim webform As NotesDocument Dim tmp As String Dim tmparr As Variant Dim tmparg As Variant Dim i As Integer '*** Get document context (in-memory NotesDocument) Set webform = session.DocumentContext '*** Get HTTP GET argument(s) after ?OpenAgent tmp = FullTrim(StrRight(webform.GetItemValue("Query_String")(0),"&")) If tmp = "" Then '*** Get HTTP POST argument(s) after ?OpenAgent tmp = FullTrim(StrRight(webform.GetItemValue("Request_Content")(0),"&")) End If '*** Separate name-value pairs from each other into array tmparr = Split(tmp,"&") '*** Loop through array, split each name-value/argument For i = LBound(tmparr) To UBound(tmparr) tmparg = Split(tmparr(i),"=") p_urldata(LCase(tmparg(0))) = Decode(tmparg(1)) Next End Sub %REM Function GetValue Description: Get value for specified argument. Returns a string containing the value. %END REM Public Function GetValue(argname As String) As String If IsElement(p_urldata(LCase(argname))) Then GetValue = p_urldata(LCase(argname)) Else GetValue = "" End If End Function %REM Function IsValue Description: Check if specified argument was passed in URL or not. Returns boolean value (True or False). %END REM Public Function IsValue(argname As String) As Boolean If IsElement(p_urldata(LCase(argname))) Then IsValue = True Else IsValue = False End If End Function '*** Private function for this class '*** There is no good/complete URL decode function in Lotusscript Private Function Decode(txt As String) As String Dim tmp As Variant Dim tmptxt As String tmptxt = Replace(txt,"+"," ") tmp = Evaluate(|@URLDecode("Domino";"| & tmptxt & |")|) Decode = tmp(0) End Function End Class It is now very easy to use the class to check what values are passed to the agent. Below is a sample agent: Option Public Option Declare Use "Class.URL" Sub Initialize Dim url As URLData '*** Create new URLData object Set url = New URLData() '*** MIME Header to tell browser what kind of data we will return Print "content-type: text/html" '*** Check reqired values for this agent If url.IsValue("name")=False Then Print "Missing argument 'name'." Exit Sub End If '*** Process name argument If url.GetValue("name")="" Then Print "'Name' is empty." Else Print "Hello, " + url.GetValue("name") + "!" End If End Sub It is that easy. If my proposal for a session at ConnectED is accepted, you will about how to use jQuery and Bootstrap to retrieve data in .NSF databases through Lotusscript agents, and I will be using…

4 Comments

Code snippet – DateClass

Here is a small Lotusscript class I wrote some years ago. I use it in a number of other classes where I need to use date functionality of different kind. For example, I have a class that communicates with a FoxPro database, using a COM object. Some of the methods in that class uses XML while other just pass a few arguments to the COM object. The COM object expects the date values to be in ISO 8601 format (yyyy-mm-dd). In addition, sometimes the date comes from a field in a Notes document where they usually are stored in US format (mm/dd/yyyy), sometimes it is the current date. So I decided to create a this class to just make the code cleaner and to avoid having to do the same conversions over and over again. This class can of course be extended with more functionality if you like. I simply put the class in a script library called "Class.Date" and then use that script library in my other classes or agents. Class DateClass Private dt As NotesDateTime Public ErrorMsg As String Public Sub New(value As Variant) Dim datestring As String ' *** Check what data type was passed and take actions. ' *** If value is blank or Nothing, use today's date. Select Case Typename(value) Case "EMPTY" : datestring = Format$(Today(),"Short Date") Case "STRING" : If Fulltrim(value) = "" Then datestring = Format$(Today(),"Short Date") Else datestring = value End If Case "DATE" : datestring = Cstr(value) End Select ' *** Also check that the value is a valid date If Isdate(datestring) = False Then ErrorMsg = "Class.Date:New() - '" & datestring & "' received is not a valid date." Set dt = Nothing Exit Sub End If ErrorMsg = "" Set dt = New NotesDateTime(datestring) End Sub Public Function DateOnly As String ' *** Return date-part only, in format selected by the system DateOnly = dt.DateOnly End Function Public Function DateOnlyISO As String ' *** Return date-part only, in ISO 8601 (big endian) standard format DateOnlyISO = Format$(dt.dateOnly,"yyyy-mm-dd") End Function Public Function DateOnlyUS As String ' *** Return date-part only, in US (middle-endian) format DateOnlyUS = Format$(dt.dateOnly,"mm/dd/yyyy") End Function End Class And this is how I use the class, this is the first few lines of a function in another script library: Public Function GetPolicyData(Byval policynumber As String, Byval lossdate As Variant) As Integer Dim DoL As DateClass Dim result As Integer Set DoL = New DateClass(lossdate) If DoL Is Nothing Then '*** Display message, including error message from DateTime class MsgBox = |Failed to initialize New DateTimeClass with LossDate "| & _ lossdate & |". | & DoL.ErrorMsg Exit Function End If '*** Call COM object with policy number and date of loss in ISO 8601 format result = object.GetPolicyData(policynumber, DOL.DateOnlyISO()) ... There you have it. Easy, isn't it?

0 Comments

Code snippet – Disable agent using external file

Yesterday I was asked to create a way to let us disable agents running on a Domino server in an easy way before the Domino server comes back from a crash. The reason for this request is that for a while we have been having one particular agent crash, taking the whole Domino server down with it. It only happens occasionally, and seems to be related to the document being processed. When the server comes up after a crash like that, a consistence check is done, then the agent manager launches the agent again, causing the server to go down again. I added code to the offending agent, so it would flag the document before processing and un-flag after processing is done. This way, when the agent encounters an already flagged document, it will be skipped as it was processed during a previous crash. For some reason this did not work yesterday morning, when one of those rare corrupted(?) documents was encountered. The logic in the code was faulty, because the document was of a new type, so it was never flagged as being processed. The same document was processed over and over again, taking the server down every time. So I simply created two functions, put them in a global script library where I keep utility functions used in many places, and added 3 lines of code to each agent where I wanted this functionality. The first function is simply to check if a specified file exists. I am using en error handler to catch any error (for example missing directory). Function FileExists(filename As String) As Boolean On Error GoTo errHandler If Dir$(filename)<>"" Then FileExists = True Else FileExists = False End If exitFunction: Exit Function errhandler: FileExists = False Resume exitFunction End Function The second function is the one where I check for the existance of a file named the same as the agent, with an extension of .disabled. If that file does not exist, I check for a file with the extension .enabled. If that file is missing, I simply create a blank file with that name. This way, the first time any agent is executed, the file will be created for us, and I don't have to sit and manually create them all. Function DisableAgent() As Boolean Dim session As New NotesSession Dim agentname As String Dim filename As String agentname = session.CurrentAgent.Name filename = "D:\NotesAgentControlFiles\" + agentname + ".disabled" If FileExists(filename) Then DisableAgent= True Else filename = "D:\NotesAgentControlFiles\" + agentname + ".enabled" If Not FileExists(filename) Then Open filename For Output As #1 Print #1, "" Close #1 Print "Created control file " & filename End If DisableAgent= False End If End Function Finally, in each agent I want to be able to disable like this, I add this code in the beginning: '*** Check if disable-file exists, exit in that case If DisableAgent() Then Exit Sub End If Just a few lines of code, but hopefully it will save someone a few minutes of work. Of course, you…

0 Comments

Code snippet – jQuery

This morning I was working on a web application, and I came up with a pretty neat and simple little solution. So I just wanted to share it, in case anyone else need something similar. I have a webpage with an HTML form. Each input tag has an attribute called notesfield, matching the name of the field in Notes where the value is stored: <div class="col-md-3"> <label>First Name</label> <input class="form-control" type="text" notesfield="FirstName" value="" /> </div> <div class="col-md-2"> <label>Initial</label> <input class="form-control" type="text" notesfield="MiddleInitial" value="" /> </div> <div class="col-md-3"> <label>Last Name</label> <input class="form-control" type="text" notesfield="LastName" value="" /> </div> Then I created a simple function that will call an agent on the Domino server, which will return all the fields on the specified document as JSON. This function is called after the HTML page is fully loaded. function loadNotesFields(docunid) { var notesfieldname = ""; $.ajax({ url: "/database.nsf/ajax_GetNotesFieldFields?OpenAgent", data: {"NotesUNID":docunid}, cache: false }).done(function(data) { $('input[notesfield]').each(function() { notesfieldname = $(this).attr("notesfield"); $(this).val(data[notesfieldname]); }); }); } The function is actually extremely simple, and here you can see the power of jQuery. What I do is to perform an Ajax call to a Domino URL, passing a UNID to the agent to use in the lookup. I set cache to false, to avoid the browser from reusing previously retrieved data (this is a good thing to do if the data retrieved can be suspected to change frequently). The jQuery .ajax() functions returns the JSON in the data object, and when the call is done, the callback function loops through each input element with an attribute of notesfield, reads the value of said attribute and then sets the value of the input element to the corresponding Notes value. The only thing left is to write the agent that will return the JSON. It could look something like this: Dim urldata List As String Sub Initialize Dim session As New NotesSession Dim webform As NotesDocument Dim db As NotesDatabase Dim doc As NotesDocument Dim urlstring As String Dim urlarr As Variant Dim urlvaluename As Variant Dim i As Integer Dim json As String Set webform = session.DocumentContext '*** Remove leading "OpenAgent" from Query_String urlstring = StrRight(webform.Query_String_Decoded(0),"&") '*** Create list of arguments passed to agent urlarr = Split(urlstring,"&") For i = LBound(urlarr) To UBound(urlarr) urlvaluename = Split(urlarr(i),"=") urldata(urlvaluename(0)) = urlvaluename(1) Next Set thisdb = session.CurrentDatabase '*** Create content header for return data Print "content-type: application/json" '*** Get Notes document baed on NotesUIND argument Set doc = db.GetDocumentByUNID(urldata("NotesUNID")) '*** Build JSON for all fields in document except $fields json = "{" + Chr$(13) ForAll item In doc.Items If Left$(item.Name,1)"$" Then json = json + |"| + item.Name + |":"| + item.Text + |",|+ Chr$(13) End If End ForAll '*** Remove trailing comma and line break json = Left$(json,Len(json)-2) json = json + "}" '*** Return JSON Print json End Sub Happy coding!

6 Comments

Code – Mask text to remove PII

Sometimes you need to remove personal identifiable information (PII) from the data you present in an application or on a web page. In the last couple of weeks this issue popped up twice, including one application which needs to be be HIPAA compliant. One solution is to mask any personal identifiable data so that the recipient can still verify the information, without sending it all in clear. I am sure you all seen this on for example credit card statements, with only the last 4 digits of your credit card number displayed. I wrote a simple  Lotusscript function to do this, and I thought I would share it so others can use it as well. You pass a string to mask, the number of characters to leave un-masked and where the unmasked characters should be displayed ("B" for beginning or "E" for end). MsgBox masktext("TexasSwede",3,"B") This line would display Tex******* MsgBox maskText("1234567890",4,"E") This line would display ******7890 Enjoy!   %REM Function maskText Description: Masks a text with asterisks, leaving the num first or last characters visible. Direction is "B" (beginning) or "E" (end). Created by Karl-Henry Martinsson - texasswede@gmail.com %END REM Function maskText(value As String, num As Integer, direction As string) As String Dim tmp As String Dim i As Integer If Len(value)>num Then If Left$(UCase(direction),1)="B" Then ' Start at the beginning tmp = Left$(value,num) For i = num+1 To Len(value) tmp = tmp + "*" Next Else ' Start at the end tmp = Right$(value,num) For i = Len(value) To num+1 Step -1 tmp = "*" + tmp Next End If Else tmp = value End If maskText = tmp End Function

4 Comments

Class for Domino Directory lookups

In many of my Notes programs, I need to perform lookups into the Domino Directory (the database formerly known as Name and Address Book or NAB). So my solution was to create a class that handle those lookups for me, and exposes the most common lookups as separate methods. We have a slightly modified version of names.nsf, with a few added fields. One of them is what we call ParallelID, which is the user's ID in a system called (surprise!) Parallel. Since I perform that lookup all the time, I created a separate method for that one called GetParallelID(). Same with manager lookup for a user, I created GetManagerName() for that. The methods you probably will use the most are GetText() and GetValue(). Since I think this class could come in handy for others, here it is. Enjoy! Option Public Option Declare Class NotesAddressBook Private NABdb As NotesDatabase Private server As String Private nabname As String Public silent As Boolean Public Sub New(servername As String) me.silent = false Call LoadNABdb(servername) End Sub Public Function GetNABdoc(personname As String) As NotesDocument Dim NABview As NotesView If NABdb Is Nothing Then Call LoadNABdb("") End If If Not NABdb Is Nothing Then Set NABview = NABdb.GetView("PeopleByFirstname") Set GetNABdoc = NABview.GetDocumentByKey(ShortUserName(personname)) Else Set GetNABdoc = Nothing End If End Function Public Function database() As NotesDatabase If NABdb Is Nothing Then Call LoadNABdb("") End If If Not NABdb Is Nothing Then Set database = NABdb End If End Function Public Function GetValue(personname As String, fieldname As String) As Variant Dim NABdoc As NotesDocument Set NABdoc = GetNABdoc(personname) If NABdoc Is Nothing Then If me.silent = False then Msgbox "No document found for '" & personname & "' in " & nabname & " on " & server & ".",,"NotesAddressBook::GetNABdoc()" End If GetValue = "" Else GetValue = NABdoc.GetItemValue(fieldname) End If End Function Public Function GetText(personname As String, fieldname As String) As String Dim tmp As Variant tmp = GetValue(personname, fieldname) If IsArray(tmp) Then GetText = CStr(tmp(0)) Else GetText = CStr(tmp) End If End Function Public Function GetName(personname As String, fieldname As String) As NotesName Dim tmpValue As String tmpValue = GetText(personname, fieldname) If tmpValue <> "" Then Set GetName = New NotesName(tmpValue) End If End Function Public Function GetNameByParallelID(parallelid As String) As String Dim view As NotesView Dim doc As NotesDocument Dim tmpValue As String Set view = NABdb.GetView("(LookupUserID)") Set doc = view.GetDocumentByKey(parallelid) If doc Is Nothing Then Exit Function End If tmpValue = doc.GetItemValue("FirstName")(0) & " " If doc.GetItemValue("MiddleInitial")(0)<>"" Then tmpValue = tmpValue & doc.GetItemValue("MiddleInitial")(0) & " " End If tmpValue = tmpValue & doc.GetItemValue("LastName")(0) If tmpValue <> "" Then GetNameByParallelID = tmpValue End If End Function Public Function GetCommonName(personname As String, fieldname As String) As String Dim tmpName As NotesName Set tmpName = GetName(personname, fieldname) If Not tmpName Is Nothing Then GetCommonName = tmpName.Common End If End Function Public Function GetManagerName(personname As String) As String GetManagerName = GetCommonName(personname, "Manager") End Function Public Function GetParallelID(personname As String) As String GetParallelID = GetText(personname, "ParallelID") End Function Public…

1 Comment

My IBM Notes project on GitHub

I decided to play around a little, and as an experiment put up one of my Notes projects on the open source repository GitHub. You can see the result here: http://github.com/TexasSwede/Class.MailMerge This is a script library in Lotusscript to create documents based on a source document and a template document. I have blogged about it before, but I added some functionality to it, and thought it would be easier for people to download a complete database.  

0 Comments

Export Notes view to Excel – with multi-value fields

A few days ago, a question was asked on StackOverflow about how to export the content of a Notes view to Excel. The caveat was that some columns contained multiple values, but not on all documents. To solve this, I wrote a Lotusscript class that will export view data as either CSV or as an HTML table, both can then be saved to a file and opened in Excel. I am posting the code below. Enjoy!   %REM Agent View Export Created Mar 27, 2013 by Karl-Henry Martinsson Description: Code to export a specified view as CSV. Copyright (c) 2013 by Karl-Henry Martinsson This code is distributed under the terms of the Apache Licence Version 2. See http://www.apache.org/licenses/LICENSE-2.0.txt %END REM Option Public Option Declare Class RowData Public column List As String Public Sub New() End Sub Public Sub SetColumnHeader(view As NotesView) Dim viewcolumn As NotesViewColumn Dim cnt As Integer ForAll vc In view.Columns Set viewcolumn = vc column(CStr(cnt)) = viewcolumn.Title cnt = cnt + 1 End Forall End Sub Public Sub SetColumnValues(values As Variant) Dim cnt As Integer Dim tmp As String ForAll v In values If IsArray(v) Then ForAll c In v tmp = tmp + c + Chr$(13) End ForAll column(CStr(cnt)) = Left$(tmp,Len(tmp)-1) Else column(CStr(cnt)) = v End If cnt = cnt + 1 End ForAll End Sub End Class Class CSVData Private row List As RowData Private rowcnt As Long %REM Function New Description: Open the view and read view data into a list of RowData objects. %END REM Public Sub New(server As String, database As String, viewname As String) Dim db As NotesDatabase Dim view As NotesView Dim col As NotesViewEntryCollection Dim entry As NotesViewEntry Dim colcnt As Integer Set db = New NotesDatabase(server, database) If db Is Nothing Then MsgBox "Could not open " + database + " on " + server,16,"Error" Exit Sub End If Set view = db.GetView(viewname) If view Is Nothing Then MsgBox "Could not access view " + viewname + ".",16,"Error" Exit Sub End If Set col = view.AllEntries() rowcnt = 0 Set entry = col.GetFirstEntry() Set row("Header") = New RowData() Call row("Header").SetColumnHeader(view) Do Until entry Is Nothing rowcnt = rowcnt + 1 Set row(CStr(rowcnt)) = New RowData() Call row(CStr(rowcnt)).SetColumnValues(entry.ColumnValues) Set entry = col.GetNextEntry(entry) Loop End Sub %REM Function CSVArray Description: Returns a string array of CSV data by row %END REM Public Function CSVArray() As Variant Dim rowarray() As String Dim textrow As String Dim cnt As Long ReDim rowarray(rowcnt) As String ForAll r In row textrow = "" ForAll h In r.column textrow = textrow + |"| + Replace(h,Chr$(13),"\n") + |",| End ForAll rowarray(cnt) = Left$(textrow,Len(textrow)-1) cnt = cnt + 1 End ForAll CSVArray = rowarray End Function %REM Function HTMLArray Description: Returns a string array of HTML data by row %END REM Public Function HTMLArray() As Variant Dim rowarray() As String Dim textrow As String Dim cnt As Long ReDim rowarray(rowcnt) As String ForAll r In row textrow = "" ForAll h In r.column textrow = textrow +…

36 Comments

How to write better code in Domino Designer – Part 3

Welcome to the third part of this series of articles about how to write better code for the Notes/Domino platform. In part 1 I wrote about creating your forms in a way so you get an instant idea of how they work (what is hidden, computed-for-display fields, etc) and in part 2 the subject had moved to Lotusscript, more specifically variable names and comments. As already mentioned in that last article (as well as in some of the comments), breaking out code into functions is a great way to make code easier to read as well as dramatically easier to maintain. That is what I will focus on in this article.   Functions In Lotusscript, the functions are declared the same way as in Visual Basic: Function YourFunctionName(argument As DataType) As ReturnDataType YourFunctionName = ReturnValue End Function You can have any number of arguments, from none to many. However, it is often suggested to keep the number of arguments to a minimum. I try to not use more than three arguments, unless there is no way around it. If you need to send a lot of information into a function, use an array or a custom data type instead. Then it is easier to change the arguments later, without changing the signature of the function. I have occasionally seen that cause issues in nested script libraries. Also, with many arguments the function declaration will probably not fit on your screen, unless you wrap the line. I try to always keep code visible in the editor, as it is easy to miss things when you have to scroll sideways to view code. So let's take a look at one of the code samples in my last article, the one where we call names.nsf to get the email address for the current user. Let's make this a function (like Kenneth Axi suggests in his comment), but we will make it a more generic function, returning the email address for any user present in address book, not just current user. Here is the original code: key = session.CommonUsername Set view = db.GetView("People") Set doc = view.GetDocumentByKey(key) emailaddress = doc.GetItemValue("InternetAddress") Below is how I would write the function. We will talk more about error handling later, but you want to make sure to always check return values before you use them. The function takes one argument, the name of the user we want the email address for, and returns the email address, or blank if no user/address was found. The username must be in the same format as the names are being displayed in the view we doing the lookup in. Function getEmailAddress(username As String) As String Dim personname As NotesName Dim nabdb As NotesDatabase Dim nabview As NotesView Dim persondoc As NotesDocument If username = "" Then MsgBox "Username is empty. Exiting.",,"No Value" Exit Function End If '*** Create NotesName object and get CommonName part. '*** We do this so username argument can be in any format. Set personname = New NotesName(username) '*** Use Domino Directory on main server Set nabdb = New NotesDatabase("Domino1/MyDomain","names.nsf") If nabdb Is…

2 Comments

How to write better code in Domino Designer – Part 2

We already talked about how to make your forms and views easier to maintain. But in most Notes/Domino applications, a majority of the code is written in Lotusscript (at least for traditional applications, XPages are outside the scope of this discussion for now). It is not hard to write easy to read Lotusscript code. Just follow some simple rules. As always, there are more than one way to do things, but this is how I do it (most of the time). I found that this works for me.   Variables Always use Option Declare (or Option Explicit) in all your code. This will prevent you from getting errors later when (not if!) you spell a variable wrong. Option Declare forces you to declare all variables before you use them. It also helps you by warning if a function or method is called with a variable of the wrong data type. Most of your bugs can be avoided using Option Declare. Use it! Give your variables meaningful names. There is no need to abbreviate variables and/or give them cryptical names, or  you will just spell them wrong later. Use a naming system that makes sense to you. As I mentioned in my previous post, I avoid Hungarian notation, it just makes the variables hard to read. Since Lotusscript is a strongly typed language, the compiler is taking care of data types. I however do use some indicators for certain variables. For the few global variables I use, I do prefix them with g_, as I then can give local variables a similar name. By giving your variables meaningful names, the code also becomes largely self-documenting. Be consistent in how you capitalize your variables. I normally write all variables in lower case only, but that is my personal preference. That way they are easy to distinguish from built-in Lotusscript functions and properties of objects. Some programmers prefer some version of CamelCase, which is fine. I occasionally use that, especially in Javascript. Be consistent in how you name related variables. If you have a variable named startpos to indicate the beginning position of a string within another string, name the other variable endpos, not posend. Think through all variables you need so you give them good names. The variables posstart and posend are not as easy to read and understand as startpos and endpos. Of course, using CamelCase, posStart and posEnd are much easier to decypher. Use the same/similar naming convention for the Domino objects as IBM uses in the online help. Use ws, uidoc, session, db, doc, etc. If you need more than one object of any type, e.g. NotesDatabase, name them in a consistent way. I always use thisdb for the database the code is executing in, nabdb for names.nsf, archivedb for (you guessed it!) an archive database, etc. Declare the variables at the beginning of the code/subroutine/function. Don't declare them throughout the code, this makes it much harder to find them later, when you perhaps need to see what data type they are. Declare the variables in…

19 Comments

Which is faster, ColumnValues() or GetItemValue()?

In a recent discussion thread in one of the forums on IBM developerWorks, the issue of which is faster, doc.GetItemValue() or viewentry.ColumnValues(). I decided to test this, using two Lotusscript agents, created to be as similar as possible, working against the same view of the same database. First, here is Agent #1, using ColumnValues() to get the value we are looking for from the first column of the view. This agent took 66.3 seconds to run, and below you can see exactly how that time was spent:   And this is Agent #2, identical to Agent #1, except two lines, first getting the NotesDocument from the view entry, and then using GetItemValue() to get the value out of the document. This agent took 225.8 seconds to run: In both agents, the call to get the next document in the ViewEntryCollection takes about 60 seconds. As you can see, the 30,000 calls to GetColumnValues() in agent #1 takes pretty much no time -- 1.3 seconds -- while it takes 133 seconds to open the 30,000 documents and read the value of the field from each one in agent #2. Almost exactly 100 times longer! In agent 2, you also have to add 26 seconds to get the NotesDocument object from the ViewEntry object. I hope this settles the discussion.  

3 Comments

Code: Expanded Class for File Functions

Yesterday I blogged about a simple class to parse file names, and that inspired me to improve it and add some functionality, which will actually come in handy for a project at work shortly. The class is pretty self-explanatory, there is really nothing complicated in the code. When the class is initialized, if a path to a directory (i.e. ending with \) is passed to the constructor the directory is created if it does not exist. If the directory exist, there are functions to copy or move both single files or all files in the directory. Directories can also be deleted using the RemoveDir method. In addition, there are properties to get the path, file name, extension and file size (in bytes) of the file (if the class was initialized with a file name). Here is an agent with some examples of how to call the class: Option Public Option Declare Use "Class.FileFunctions" Sub Initialize Dim file As FileObject Dim cnt As Integer Dim success As Boolean '*** Create new file object Set file = New FileObject("D:\Downloads\Downloads\MERP\Assassins of Dol Amroth.pdf") '*** Copy the file to another (new) directory Call file.CopyTo("D:\Downloads\MERP1\", file.FileName) '*** Move the file to a new location and replace space with + in file name Call file.MoveTo("D:\Downloads\MERP2\", Replace(file.FileName," ","+")) '*** Create a new directory if it does not exist Set file = New FileObject("D:\Downloads\MERP3\Test\") '*** Copy all files in specified directory to another directory Set file = New FileObject("D:\Downloads\Downloads\MERP\") cnt = file.CopyAllTo("D:\Downloads\MERP\Backup\") MsgBox "Copied " & cnt & " files." '*** Move all files in the previously specified directory to another location cnt = file.MoveAllTo("D:\Downloads\Middle-Earth Role Playing Game\") MsgBox "Moved " & cnt & " files." '*** Remove D:\Downloads\Downloads\MERP\ Call file.RemoveDir("") '*** Remove D:\Downloads\MERP3\ and Test directory that we created earlier success = file.RemoveDir("D:\Downloads\MERP3\Test\") If success = True Then success = file.RemoveDir("D:\Downloads\MERP3\") If success = False Then MsgBox "Failed to delete D:\Downloads\MERP3\" End If Else MsgBox "Failed to delete D:\Downloads\MERP3\Test\" End If End Sub   Below is the class itself, I put it in a script library called Class.FileFunctions. %REM Copyright (c) Karl-Henry Martinsson 2012. Some code copyright Andre Guirard (see below). You are free to use and modify my code, as long as you keep all copyright info intact. If you improve the code, please consider sharing it back to the community. %END REM Option Public Option Declare Type FileDetails path As String filename As String extension As String filesize As Long End Type Class FileObject Private file As FileDetails Public silent As Boolean Public Sub New(filepathname As String) silent = False FullPathName = filepathname If file.FileName = "" Then If file.Path "" Then On Error 76 GoTo parentDoesNotExist 'No filename but path, then we create that directory (if missing) If Dir$(file.Path,16)="" Then createDirectory: Call MakeDir(file.Path) End If End If file.FileSize = 0 Else file.FileSize = FileLen(filepathname) End If Exit Sub parentDoesNotExist: Resume createDirectory End Sub Public Property Set FileName As String file.FileName = FileName file.Extension = StrRightBack(FileName,".") End Property Public Property Get FileName As String FileName…

0 Comments

Show and Tell – Dynamic Web Calendar

In this post in the developerWorks forum, Andy Herlihy is asking about how to create a small calendar that can be embedded into an existing intranet website. As Carl Tyler points out, Andy could simply use Dojo. However, this is a good example to show some jQuery combined with the power of IBM Lotus Domino.  Lets' break it down into a few smaller steps. The first one is to create a Domino web agent that will take the current date and build a calendar for that month. There will be no links or javascript code in the HTML generated, just plain HTML code, with some id attributes to make it easy to address different cells later. The next step is to create a very simple webpage, with some Javascript to load jQuery and set up the calendar to detect clicks. The clicks will cause another call to the Domino server, this time to a view using RestrictToCategories to only get the entries for the specified date. The entries are returned as HTML and through jQuery they are displayed in a div on the webpage. We also want to add a little bit of CSS to make the calendar pretty. The CSS also lives on the HTML page. Finally we create the view, it's associated view template form and a form to create some entries that we can use for testing.   Let's look at the code. First the Domino web agent. It should be pretty self explanatory: %REM Agent WebCalendar Created Nov 26, 2012 by Karl-Henry Martinsson/Deep-South Description: Returns HTML for a calendar for current month %END REM Option Public Option Declare Sub Initialize '*** Used to get URL params Dim session As New NotesSession Dim webform As NotesDocument Dim urlstring As String Dim urlarr As Variant Dim urlvaluename As Variant Dim urldata List As String Dim webservername As String '*** Agent specific Dim startDate As NotesDateTime Dim endDate As NotesDateTime Dim i As Integer Dim j As Integer Dim startweekday As Integer Dim thisMonth As Integer Dim currDate As Double Dim cellclass As String Set webform = session.DocumentContext '*** Calculate path for this database, for image/icon file references later webservername = webform.GetItemValue("Server_Name")(0) '*** Remove leading "OpenAgent" urlstring = StrRight(webform.Query_String_Decoded(0),"&") If urlstring <> "" Then '*** Get all params passed to agent urlarr = Split(urlstring,"&") For i = LBound(urlarr) To UBound(urlarr) urlvaluename = Split(urlarr(i),"=") urldata(urlvaluename(0)) = urlvaluename(1) Next If IsElement(urldata("date")) = False Then urldata("date") = Format$(Now(),"mm/dd/yyyy") End If Else urldata("date") = Format$(Now(),"mm/dd/yyyy") End If '*** Get first and last date of current month Set startDate = New NotesDateTime(Format$(urldata("date"),"mm/01/yyyy")) Set endDate = New NotesDateTime(startdate.DateOnly) Call endDate.AdjustMonth(1) Call endDate.AdjustDay(-1) currDate = CDbl(CDat(startDate.DateOnly)) startweekday = Weekday(Cdat(startDate.Dateonly)) '*** HTML header Print "Content-type: text/html" ' & CRLF Print |<table class="calendar">| '*** Create calendar header with weekdays Print |<tr><td colspan=7 class="labelMonthYear">| + Format$(CDat(startDate.DateOnly),"mmmm yyyy") + |</td></tr>| Print |<tr class="calendarHeader">| Print |<td>S</td><td>M</td><td>T</td><td>W</td><td>T</td><td>F</td><td>S</td>| Print |</tr>| '*** Build rows for the weeks For i = 1 To 6 Print |<!-- Start row | & i & | -->| Print |<tr class="calendarRow" id="calendarRow| & i…

1 Comment

How to detect changes in fields on a form

A question I have seen a few times in the developerWorks forums is how to detect what values (often names) have been added in a multi-value field. This is not very difficult to do in Lotusscript, and there are a few different ways to do it. I prefer to use lists, but you can of course use arrays as well. The basic idea is to declare a global variable to hold the initial value of the field, populate it when the document is either opened or put into edit mode, and then checked when the document is saved. Globals: Dim oldValue List As String PostModeChange: Dim oldarray as Variant '*** Create array, assume ; as item separator in field oldarray = Split(uidoc.FieldGetText("myMultiValueField"),";") '*** Build a list of values ForAll o in oldarray oldValue(o) = o End ForAll QuerySave: Dim newarray as Variant Dim newValue List As String '*** Create array, assume ; as item separator in field newarray = Split(uidoc.FieldGetText("myMultiValueField"),";") '*** Compare with values in list ForAll n in newarray '*** Check if value is not in global list, then it is new If IsElement(oldValue(n)) = False Then newValue(n) = n ' Add new value to list End If End ForAll '*** Loop through all new values and print them ForAll nv in newValue Print nv + " is new." End Forall The same technique can be used to detect what fields have changed since the document was opened. Just store the values of all fields (except any starting with $) in a list, then check the values in the fields against that list when saving the document. Globals: Dim oldValue List As String QueryOpen: '*** Build a list of values with field name as listtag ForAll i in doc.Items If Left$(i.Name,1)"$") Then oldValue(i.Name)=i.Text End If End ForAll QuerySave: Dim modified List As String Dim tmpValue As String '*** Compare current fields with values in list ForAll o in oldValue '*** Check if value is the same or not tmpValue = uidoc.FieldGetText(Listtag(o)) ' Get current field value If tmpValue o Then modified(ListTag(o))=tmpValue ' Add new value to list of modified fields End If End ForAll '*** Loop through all modified fields and display new value ForAll m in modified Print Listtag(m) + " was changed from " + oldValue(Listtag(m)) + " to " + m End Forall All very easy when you know how to do it. And again, it shows the power of lists.

3 Comments

Export from Notes to Excel – 3 different ways

While reading the developerWorks forums, I have noted that some tasks seem to be common, and users often have issues with it. One very common is to export data from Notes/Domino to Excel. My guess is that some manager/executive want to get the data to manipulate and analyze it in Excel, which they know better, and where reporting can be done easier. First of all, for anyone that want to do real integration with Excel (or Word, or any other office program), I would suggest to look at the presentations given by John Head at Lotusphere and other conferences in the past: Lotusphere 2011 - JMP208 IamLUG 2009 - Integration and Coexistance Lotusphere 2006 - BP309 Lotusphere 2005 - JMP108 But if you just need to export raw data to Excel, there are a couple easy ways to do that. You don't need to automate Excel, something the programmers posting in the forums seem to frequently do. That just makes things more complicated, and if you run the code in a server agent, Microsoft Office have to be installed on the server itself. Different versions of Excel also cause different issues. The different methods I use to export Notes data to Excel are as follows: Create a CSV file, which can be opened in Excel Create a HTML table in a file with .xls extension, which can be opened in Excel Create a HTML table and return it through a browser, with a content type set to open in Excel. Depending on what the requirement is, I usually choose one of those method.   Method 1 - CSV file (local) The easiest way to get data from Notes to Excel (or any other spreadsheet program) is to simply save the data as  comma-separated values (CSV). Use regular Lotusscript file operations and write the values to the file, encased in double quotes and separated by a comma. You need to build a string containing a complete line, as the Print statement adds a line break to the end automatically. A CSV file could look like this: FirstName,LastName,Department,StartYear "Karl-Henry","Martinsson","IT","2002" "John","Smith","Accounting","2009"   The first row is the header, with the field names listed. This helps on the receiving side, but is optional. Dates and numbers don't need quotes around them, as long as they don't contain commas. So make a habit to format any numbers without commas.   Method 2 - HTML file (local) This method is very similar to the CSV method. The difference is that you create a table in HTML, matching the cells in Excel you want to create/populate. But instead of creating a file with the extension .htm or .html, you give it the extension .xls. Excel is intelligent enough to identify it as HTML and map the content correctly. The biggest advantages of using a HTML file instead of CSV is that you get more control over the formatting. By using colspan and rowspan, you can merge cells together, for example in a header. You can also use markup to make cells bold or italic, or even set the…

14 Comments

Moving blog posts from Connections to WordPress

As I switched from IBM Connection to WordPress for my blog, I started thinking about my existing content. Was there a way to move them all over without having to manually copy and paste and recreate all 268 entries? Well, there is, and this is how I did it, using just a  few tools. First I used Wget to retrieve my old blog. This put all the posts on one folder (entries), and all images in another (resource). It was then a simple task to write a Lotusscript agent that processed each file in that folder and read the content, parsed out the title, date originally posted and HTML for the blog post itself. I put that data into separate Notes documents, after performing some cleanup and string replacement. I had already moved all images to a filer on my primary web server, so I performed a replace of the image URLs in the HTML, to have any images pointing to their new location. I also had to fix some special characters and replace them with the corresponding HTML entities. Now when I had all the data, I just wrote another agent to export the data out again, to create a CSV file. I then installed a CSV importer in my WordPress blog and used to to import the file I just created. After a few tweaks I performed a successful import. Later I realized I had missed a few special characters, so I had to fix those entries, but we are talking about 4 or 5, out of 268 entries. If there is an interest, I might clean up the code a little and create a nicer UI (right now many of the values like path and URL are hard-coded) and then release the code if anyone else is planning to go through the same exercise. Below is the existing code to read the blog entries into a simple Notes database. Option Public Option Declare Dim entrydir As String Dim resourcedir As String Sub Initialize Dim filename As String Dim cnt List As Integer Dim blogentry List As String Dim tst As Variant entrydir = "D:\BleedYellowBlog\www.bleedyellow.com\blogs\texasswede\entry\" resourcedir = "D:\BleedYellowBlog\www.bleedyellow.com\blogs\texasswede\resource\" cnt("Total") = 0 filename = Dir$(entrydir + "*.*") Do While fileName <> "" blogentry(filename) = entrydir + filename cnt("Total") = cnt("Total") + 1 fileName = Dir$() Loop cnt("Processed") = 0 ForAll be In blogentry cnt("Processed") = cnt("Processed") + 1 Print "Processing " & cnt("Processed") & " of " & cnt("Total") Call ProcessBlogEntry(ListTag(be),be) End ForAll End Sub Function FixHTML(html As String) As String Dim tmp As String tmp = Replace(html,_ "https://www.bleedyellow.com/blogs/texasswede/resource/",_ "http://www.texasswede.com/blogfiles/resource/") tmp = Replace(tmp,_ "http://www.bleedyellow.com/blogs/texasswede/resource/",_ "http://www.texasswede.com/blogfiles/resource/") tmp = Replace(tmp,"/BLOGS_UPLOADED_IMAGES/","/uploaded_images/") tmp = Replace(tmp,"´",|"&acute;"|) tmp = Replace(tmp,"’","&acute;") tmp = Replace(tmp,"“",|&quot;|) tmp = Replace(tmp,"”",|&quot;|) tmp = Replace(tmp,"…",|"..."|) tmp = Replace(tmp,"<wbr>",||) tmp = Replace(tmp,"> < ",|>&anp;nbsp;< |) FixHTML = tmp End Function Function ProcessBlogEntry(filename As String, localpath As String) As Boolean Dim session As New NotesSession Dim db As NotesDatabase  Dim blogentry As NotesDocument Dim rtitem As NotesRichTextItem  Dim siteurl As String Dim html List As String…

1 Comment

Things to think about when programming in Notes

Inspired by some of the posts in the DeveloperWorks forums and on StackOverflow, I thought I would post some more basic concepts and how I handle them. I am not saying my way is the best way, this is just what works for me. I am sure there will be more posts in the future"..." I will also mention a few other things I noticed while reading the code posted in the forums.   Retrieve something that doesn´t exist The question is how to identify what dates there are no documents created for. This is where lists are very useful. Richard Schwartz answered this question and posted some good code. Rich suggests to create a list of dates, with each list item having an initial values of false, and then loop through the documents. As each document is processed, the value of the corresponding list item is changed from false to true. You can then go through the list and see which dates still have a value of false, those dates are missing documents. My version of the same code is to actually delete the list item you have a match for, instead if setting it to true. In the end you have a list of just the items of dates without a corresponding document.   Write readable code This could be a blog entry all by itself. But I notice that much of the code in the DeveloperWorkds forums is hard to read"´". Partially because any tabs or multiple spaces used for indenting the code is stripped out, but also because the posters don´t write easy-to-read code. Variable names are often not descriptive: Dim db1 As NotesDatabase Dim db2 As NotesDatabase vs Dim thisdb As NotesDatabase Dim nabdb As NotesDatabase Which one is easier to understand? In my opinion (and I am sure you agree) the second variant. Also function names and other variables should be named so you understand what they do and what kind of data they contain. Comments are mostly non-existing. It is not that hard to add some comments to the code that explain what the code is doing. But don´t explain every line of actual code (it should be self-explanatory, if variables are named correctly), explain what a particular section of code is intended to do. Here is a section of code from an agent I wrote earlier this week: '*** Read PhotoUNID field in LossControl document'*** and build a list of the UNID values in the fieldphotoUNID = lcdoc.GetItemValue("PhotoUNID")(0)If photoUNID<>"" Then '*** Create array of values and put into photolist tmparray = FullTrim(Split(photoUNID,";")) ForAll t in tmparray If t <> "" Then photolist(t) = t End If End ForAll End If The comments above will help the next person to look at the code to quickly understand what it is intended to do.   More on variables Use Option Declare/Option Explicit. This will find many errors, especially for more inexperienced programmers, where variables are misspelled or missing, something that is a very common…

0 Comments

Regular Expressions in Notes (Lotusscript)

Today I needed to use regular expressions (a.k.a. regexp) in a Lotus Notes application. I just wanted to check if the user entered a claim number (in the format "nnXXXXXnnnnn", e.g. 12RICTX12345) in a field. A quick online search found a blog entry with some code using the VBScript object available in Windows, and I adapted it for my application. Just in case someone need this, I am posting the code below. I am not taking credit for the code, I found it on Giles Hinton´s blog and just adapted it a little bit. I also found information about using LS2J and Java to handle regular expression in Notes, which should be platform independent, not restricted to just Windows. Since all our users are on Windows (either directly or through Citrix), I could use the quick method below. But I would probably use the script library posted on OpenNTF for more serious code.   Dim ws As New NotesUIWorkspace Dim uidoc As NotesUIDocument Dim regex As Variant Dim pattern As String Dim result As String Dim match As Boolean '*** Define pattern and get text value to check for match pattern = |b([0-9]{2}[a-zA-Z]{5}[0-9]{5})b| Set uidoc = ws.CurrentDocument subject = uidoc.FieldGetText("ShortDescription") '*** Create RegExp object Set regex = CreateObject("VBScript.Regexp") regex.Global = True regex.IgnoreCase = True regex.Pattern = pattern '*** Test for match of pattern in text match = regex.Test(subject) If match = True Then Msgbox "Claim number was found in the field." End If

0 Comments

End of content

No more pages to load