Posts Tagged: ‘LotusScript’

Tom Zeizels Blog: Learning by Learning – die Domino Tech School

27. Januar 2019 Posted by Thomas Zeizel, IBM

Tom Zeizels Blog: Learning by Learning – die Domino Tech School Mit Domino V10 ist die Domino Plattform wieder in den Fokus von jungen Programmierern gerückt, weil sie heute modernste Software-Entwicklungsmethoden mit unternehmensgerechter Stabilität und Funktionalität in einzigartiger Weise verbindet und damit offen für den Einsatz in beliebigen Plattformkonstellationen ist. Domino V10 ist die neutrale […]

Der Beitrag Tom Zeizels Blog: Learning by Learning – die Domino Tech School erschien zuerst auf DNUG.

EntwicklerCamp 2016: LotusScript extrem!

13. April 2016 Posted by Thomas Bahn

EntwicklerCamp
Heute war ich überwältigt, überwältigt von der Anzahl der Menschen, die sich heute in den Raum gequetscht haben. Ich erinnere mich nicht, dass der Raum (Track 2) schon mal so voll war. Vielen Dank an alle, die sich für den Vortrag entschieden haben - insbesondere für die Geduld und das Verständnis, als ich doch nicht ganz unwesentlich überzogen habe.

Die Präsentation und die Demo-Datenbank zu meinem Vortrag in Track 2, Session 8: LotusScript extrem!




EntwicklerCamp 2016: LotusScript extrem!

13. April 2016 Posted by Thomas Bahn

EntwicklerCamp
Heute war ich überwältigt, überwältigt von der Anzahl der Menschen, die sich heute in den Raum gequetscht haben. Ich erinnere mich nicht, dass der Raum (Track 2) schon mal so voll war. Vielen Dank an alle, die sich für den Vortrag entschieden haben - insbesondere für die Geduld und das Verständnis, als ich doch nicht ganz unwesentlich überzogen habe.

Die Präsentation und die Demo-Datenbank zu meinem Vortrag in Track 2, Session 8: LotusScript extrem!




LotusScript Web Services and NullPointerException

7. August 2015 Posted by Bernd Hort

DominoDesigner_90x90.png

Recently I worked on a demo application for a customer to show how to use Web Service Consumer in LotusScript. At one point I got a java.lang.NullPointerException error. It took me quite some time to figure out the reason for this error. So this blog post should help anyone how might run in the same problem. Because frankly the solution was really simple once I had found it.

I give you the summary first and the full technical explanation later in this blog post. So even if you are not interested in the details the main point to take away is:

LotusScript Web Services Consumer and Provider use internally Java and Java is case sensitive. If you do not obey this you might loose some hours of your life time chasing an error where no error is.

Here are the technical details.


One part of the demo were the different styles to use to generate the WSDL file from a Web Service Provider. There is a great article from IBM developerWorks Which style of WSDL should I use? on this topic. Highly recommended!

To be able to show the differences between RPC/encoded and Document/literal style I implemented two different Web Service Providers. Both providers had the same identical LotusScript code.

Then I implemented two Web Service Consumers just by importing the WSDL file from the previous generated Web Service Providers. Here are the two methods from the Web Service Consumers.

Web Service Consumer calling a RPC/encoded Web Service Provider
Web-Service-Provider-RPC-encoded-Settings.png

Function checkout_status(auftragnr As String) As String
  If Not IsDebugMode Then On Error Goto errorHandler
  On Error lsERR_NOTES_WSENGINE_UNINIT_METHOD_ARG Goto wsErrorHandler
  On Error lsERR_NOTES_WSENGINE_NOTINIT Goto wsErrorHandler
  On Error lsERR_NOTES_WSENGINE_ERROR Goto wsErrorHandler
  On Error lsERR_NOTES_WSENGINE_METHOD_ERROR Goto wsErrorHandler
  On Error lsERR_NOTES_WSENGINE_METHOD_FAULT Goto wsErrorHandler
  
  Let checkout_status = Service.Invoke("checkout_status", auftragnr)
  
  Exit Function
  
errorHandler:
  If HandleErrorWithContext(CreateErrorContext(Nothing,|auftragnr: "| & auftragnr & |"| )) = RESUME_NEXT_LINE Then Resume Next
  Call RethrowError()
  Exit Function
  
wsErrorHandler:
  If HandleErrorWithContext(CreateErrorContext(Nothing,|auftragnr: "| & auftragnr & |"| )) = RESUME_NEXT_LINE Then Resume Next
  Call CreateWebServiceLogEntry(Me, |auftragnr: "| & auftragnr & |"|, Nothing)
  Call RethrowError()
End Function

Web Service Consumer calling a Document/literal Web Service Provider
Web-Service-Provider-RPC-encoded-Settings.png

Function checkout_status(auftragnr As String) As String
  If Not IsDebugMode Then On Error Goto errorHandler
  On Error lsERR_NOTES_WSENGINE_UNINIT_METHOD_ARG Goto wsErrorHandler
  On Error lsERR_NOTES_WSENGINE_NOTINIT Goto wsErrorHandler
  On Error lsERR_NOTES_WSENGINE_ERROR Goto wsErrorHandler
  On Error lsERR_NOTES_WSENGINE_METHOD_ERROR Goto wsErrorHandler
  On Error lsERR_NOTES_WSENGINE_METHOD_FAULT Goto wsErrorHandler
  
  Let checkout_status = Service.Invoke("CHECKOUT_STATUS", auftragnr)
  
  Exit Function
  
errorHandler:
  If HandleErrorWithContext(CreateErrorContext(Nothing,|auftragnr: "| & auftragnr & |"| )) = RESUME_NEXT_LINE Then Resume Next
  Call RethrowError()
  Exit Function
  
wsErrorHandler:
  If HandleErrorWithContext(CreateErrorContext(Nothing,|auftragnr: "| & auftragnr & |"| )) = RESUME_NEXT_LINE Then Resume Next
  Call CreateWebServiceLogEntry(Me, |auftragnr: "| & auftragnr & |"|, Nothing)
  Call RethrowError()
End Function

Can you spot the difference? I guess it is easier after giving you the solution.

In the Document/literal version the line for actually calling the Web Service must be
Let CHECKOUT_STATUS = Service.Invoke("CHECKOUT_STATUS", auftragnr)
with capital CHECKOUT_STATUS.

I found out because I took the original generated code and enhanced it with some error handling. Since there could be not code without properly error handling. This is specially true for calling Web Service.

Since I'm a lazy programmer I copied the code from the one Web Service Consumer to the next not looking at the case. I was very confusing that the first Web Service Consumer worked and the second one returned a java.lang.NullPointerException.

After checking for all kind of reasons I finally started again with a new Web Service Consumer and then found the difference in the case.

The reason behind is the internal use of Java. While calling Service.Invoke in the background the Java Reflection API is used to find the corresponding method to call. Those two different WSDL styles lead to two different method names: checkout_status and CHECKOUT_STATUS. In LotusScript the case of a method name makes no difference. In Java it can mean you can spend hours looking for an error.

By the way if I had used Java in the first place to program the Web Service Consumer (or Provider) I would not have had this problem.

EntwicklerCamp 2015: Wenn ich das früher gewusst hätte, hätte ich schon lange objekt-orientiert programmiert

4. März 2015 Posted by Thomas Bahn

EntwicklerCamp
Objekt-orientierte Programmierung ist - immer noch - ein wichtiges Thema für mich. Umso mehr freue ich mich über das positive Feedback nach diesem Vortrag.
Und wie gesagt: Fang klein an, vielleicht mit einer Hilfsklasse für E-Mails oder für RichText, um Erfahrungen zu sammeln. Beim nächsten komplexeren Prozess (z. B. Agent), vielleicht beim Import oder Export, verwende diesmal OOP. Und dann mach eine Basis-Klasse für dich mit deinen für dich wichtigen Methoden, die du sowieso immer schon benutzt hast, und stelle mal eine Maske um...

Die Präsentation und die Demo-Datenbank zu meinem Vortrag in Track 3, Session 3: Wenn ich das früher gewusst hätte, hätte ich schon lange objekt-orientiert programmiert:



An elegant, UI-only way to link documents in an IBM Domino database with LotusScript

4. März 2015 Posted by Björn Großewinkelmann

 

Problem Description

In this article I will show you how to elegantly link documents in a Notes database with the use of event handlers. At first glance linking documents together seems to be a very trivial task. It is something we all implemented hundreds of times already, either using built-in Notes response hierarchies or through building document trees of our own design. Doing that programmatically while using backend classes is as trivial as it sounds, but letting the user do it interactively is something else entirely. Because then there are certain additional requirements that should be implemented to guarantee a consistent and hassle-free user experience.

So let us take a look at those requirements for the two standard use cases:

1. The user links an already existing document to the currently open document (through a dialog field or a view selection). The title of the linked document should be displayed in a field and the document can be opened through a hotspot.

Requirements

  • If the user doesn’t save the currently open document the link should be voided i.e. the field should be empty and the hotspot action should not execute.
  • The operation should not access backend classes to avoid replication and safe conflicts.

2. The user creates a new document from an already open document. The new document should be linked to the already opened one and its title should be displayed in a field (and can, after the initial save, opened through a hotspot).

Requirements

  • If the user doesn’t safe the new document AND the current document the link should be voided i.e. the field should be empty and the hotspot action should not execute, even if the new document has been saved.
  • The operation should not access backend classes to avoid replication and safe conflicts.
  • Every change the user performs on the new document should immediately be reflected in the old document.

 

Implementation

During the following demonstration I will call the document that provides the link the parent and the link target the child. I also will not make use of the Parent-Response-Hierarchy but will implement my own hierarchy using document IDs I generate myself programmatically. I am sure you know why that is ;-). Of course you can use a Parent-Response Hierarchy instead and, if you are feeling lucky, you can even use Universal IDs to define the link.

 

Case 1: Linking to an existing document

The first case is more or less trivial. The child already exists and will not be opened during the linking operation hence we actually can (have to actually) access it using backend classes, specifically an instance of NotesDocument or built-in functions e.g. a dialog list field.
We create a dialog list field on the parent form and populate the choices with a column of a lookup view. The column formula should be something like the following:

doc_title + “|” + doc_id

Now make sure “Allow keyword synonyms” is checked in the field properties and you are done.

If you have to actually use backend classes, for example in conjunction with the NotesUIWorkspace Functions PicklistCollelction or PicklistStrings you can use the NotesUIDocument Function FieldSetText to set the value. And yes you can set computed and even hidden fields that way, too. I normally use a dialog list field even if the field is computed and set with FieldSetText. That way I can set the doc_id and after a NotesUIDocument.Refresh the displayed value is automatically translated to the title. For this to work you of course have to check the option “Refresh choices on document refresh” in the field properties.

The hotspot action performs a lookup in the same view (the first sorted column has obviously to be doc_id) and opens the document.

 

Case 2: Linking a new document

This case requires a little more work. So let us split the requirements into two steps to make it clearer.

For the purpose of code clarity and management we create persistent backend classes which serve as a wrapper for NotesUIDocument instances. We accomplish that through creating an instance of our wrapper in the PostOpen event and saving the reference in a variable declared in the Globals section of the form. For more details read my previous articles on the subject:

Event Handling mit LotusScript Teil I - Persistente Event-Handler

 

Step 1: Creating the child

In the backend class for the parent we first declare a private member variable:

Private childUIDoc As NotesUIDocument

 

And the following sub:

Public Sub actionCreateChild()

   Dim ws As New NotesUIWorkspace()
   Dim db As NotesDatabase
   Dim childDoc As NotesDocument

   Set db = ws.CurrentDatabase.Database
   Set childDoc = db.CreateDocument()

   Call childDoc.ReplaceItemValue( "Form", "childDoc" )
   Call childDoc.ComputeWithForm( False, False )

   Set me.childUIDoc = ws.EditDocument( True, childDoc )


End Sub

 

That sub will be called by the action which we use to create the child document. In effect our persistent instance of the parent wrapper class now has a handle on the NotesUIDocument instance of the just created child.
 

Step 2: Handling Child Events in the Parent-Wrapper

Next we want to notify the parent if and when the child gets saved or closed. For that purpose we bind the corresponding events, which are fired by the child NotesUIDocument instance, to event handlers we define in our parent wrapper.

Public Sub actionCreateChild()
    
    Dim ws As New NotesUIWorkspace()
    Dim db As NotesDatabase
    Dim childDoc As NotesDocument
    
    Set db = ws.CurrentDatabase.Database
    Set childDoc = db.CreateDocument()
    
    Call childDoc.ReplaceItemValue( "Form", "childDoc" )
    Call childDoc.ComputeWithForm( False, False )
    
    Set me.childUIDoc = ws.EditDocument( True, childDoc )

    On Event PostSave From me.childUIDoc Call OnPostSave_Child
    On Event QueryClose From me.childUIDoc Call OnQueryClose_Child

End Sub

 

All the PostSave event handler has to do is to set the doc_id and refresh the NotesUIDocument instance of the parent document. This way the current title of the document is always displayed.

Public Sub OnPostSave_Child( Source As NotesUIDocument )
    
    Call me.mUIDoc.FieldSetText( "child_id", _
                                  Source.FieldGetText( "doc_id" ) )
    Call me.mUIDoc.Refresh()
    
End Sub

 

In the QueryClose event handler we first check if the child has been saved. Since we created the document using backend classes we need to check the property IsNewNote on the backend document rather than the property IsNewDoc which is provided by the NotesUIDocument instance (and hence will always be false). If the child has been saved we do nothing, everything already has been taken care of by the PostSave event handler. If it has not been saved we reset the link field to the empty string, thus removing the link.

Public Sub OnQueryClose_Child( Source As NotesUIDocument, _
                               Continue As Variant )
    
    If childUIDoc.Document.IsNewNote Then
          Call me.mUIDoc.FieldSetText( "child_id", "" )
          Call me.mUIDoc.Refresh()
    End If
    
End Sub

 

And that is it. If the child doesn’t get saved the link will be empty. If the parent doesn’t get saved the link won’t change at all. If it was empty before the child was created it will obviously stay that way regardless whether or not the child has been saved.

 

Step 3: Opening the link

To keep the behavior consistent we also have to obtain the child NotesUIDocument instance when the user follows the link. For that we create a new sub for the sole purpose of opening the document.

Public Sub actionOpenChild()

    Dim ws As New NotesUIWorkspace()
    Dim db As NotesDatabase
    Dim view As NotesView
    Dim childDoc As NotesDocument
    Dim childID As String
    
    childID = me.mUIDoc.Document.getItemValue( "child_id" )
    
    If Not childID = ""  Then
          Set db = ws.CurrentDatabase.Database()
          Set    view = db.GetView( "lkpChildDoc" )
          Set childDoc = view.GetDocumentByKey( childID, True )
        
          If Not childDoc Is Nothing Then
                Set me.childUIDoc = ws.EditDocument( True, childDoc )
          End If
    End If
    
End Sub

 

So in theory we are done but what if the child gets opened end edited outside of the parent context? Well if we use the dialog list approach we are still done because when the child gets edited the view entry will change and the link title will be updated the next time you open the parent (into edit mode). But what if you want the parent to reflect changes in the child immediately?
 
There probably is a reason why you made one document the child and the other the parent and not the other way around. You built your hierarchy to reflect some piece of business logic or maybe even real logic ;-).

In consequence it is highly likely that the user is already editing the parent when he opens the child. Hence updating the parent programmatically through the child without having the parent context invites all kinds of trouble. So don’t! Rather write an agent that runs once a night and performs a ComputeWithForm on all parent documents.


 

Updating RichTextFields on unsaved UIDocuments with LotusScript

11. Dezember 2014 Posted by Jan Santüns

 

Normally, updating RichTextFields in an opened IBM Notes document requires saving, closing and then reopening the document. If you use the RichTextField to attach a file to the document even after doing all this closing and reopening, it is not guaranteed that the attachment is inserted where you want it to be. Another problem that is implicated with this is the necessity to save documents you do not want to save, yet.

Fortunately, there is an undocumented method in LotusScript that enables us to update RichTextFields on unsaved UIDocuments without the need for saving and reopening them.

It's called

NotesUIDocument.ImportItem( tmpDoc As NotesDocument, RichTextItem As NotesRichTextItem)

and it is part of in the NotesUIDocument class.

 

Explanation standard method:

The standard method to embed files or document links etc. into RichTextFields is the use of

NotesRichTextItem.embedObject

 

Example Code:

    Dim ws As New NotesUIWorkspace
    Dim db As NotesDatabase
    Dim doc As NotesDocument
    Dim rtitem As NotesRichTextItem
    
    Set doc = ws.CurrentDocument.Document
    Set rtitem = New NotesRichTextItem(doc, "RichTextField")
    Call rtitem.EmbedObject( EMBED_ATTACHMENT, "", "C:DesktopDocument.docx")

 

Fig. 1: Notes form with buttons for the standard method and our workaround for attaching files in the RichTextField

 

After using the standard method the problems stated above become obvious. When the document is saved and reopened the file is attached at the end of the document, but not in the RichTextField where it should be.

Fig. 2: Result of the standard method EmbedObject of LotusScript

 

Explanation workaround:

Now we will use the ImportItem method.

 

Example Code:

    Dim sess As New NotesSession
    Dim ws As New NotesUIWorkspace
    Dim db As NotesDatabase
    Dim tmpdoc As NotesDocument
    Dim rtItem As NotesRichTextItem
    Dim uiDoc As NotesUIDocument
    
    'uiDoc contains the RichTextField we want to import our file into             
    Set uiDoc = ws.CurrentDocument
    
    'Create a support NotesDocument (tmpDoc)       
    Set db = sess.currentDatabase
    Set tmpdoc = db.CreateDocument
    
    'Create a RichTextItem on the support document to embed the desired file into
    Set rtItem = New NotesRichTextItem(tmpdoc, "RichTextField")
    
    'embed the desired file into the created RichTextItem
    Call rtItem.EmbedObject( EMBED_ATTACHMENT, "", "C:DesktopDocument.docx")
    Call rtItem.update
    
    '!!Using computeWithForm ensures that the attachment is completely embed into the RichTextField before
    'we try to import it into our uiDoc. It works even though we have no form specified!!

    Call tmpdoc.ComputeWithForm(False, False)
    
    'Put Cursor into the RichTextField --> precondition for the file's import
    Call uiDoc.GoToField("RichTextField")
    
    'finally import the desired file into the RichTextField on the uiDoc.
    Call uiDoc.ImportItem(tmpDoc, "RichTextField")

 

Fig. 3: Result of the workaroung method ImportItem

 

As you can see, right after using the workaround button, the file is attached into the RichTextField of our opened document as desired. No saving and reopening needed!

!Please keep in mind that this workaround method is undocumented an could be removed from LotusScript by IBM!
 
I hope this workaround helped you as much as it helped me!
If you have any questions or suggestions do not hesitate to contact us.
 

Sideeffect in LotusScript: convertToMIME on @ClientType

22. Oktober 2014 Posted by Carl Goos

 

Today I found a strange sideeffect in LotusScript. After calling the method

       NotesDocument.convertToMIME()

I used the @Function

        @ClientType

some times later in my IBM Notes Standard Client. The result from this @Formula was "Web" <- which is obviously not true ;)

Only after restarting the Notes Client @ClientType seems to be 'resetted'. I tested this behaviour on Notes 8.5.3 FP6 and Notes 9.0.1 with the same effect. I realized times ago that @ClientType does not work correctly, but I didn´t see a pattern. Now I could explain why sometimes the formula results the wrong value.

I just opened a PMR at IBM, perhaps there is someone who could tell me in the meantime if there is a possibility to 'reset' the @ClientType to normal after using NotesDocument.convertToMIME() ;) (notes.ini, other script function or so)

Many thanks!

Carl

Here is a little code for testing. Just paste it in a view action or button:

        Dim ws As New NotesUIWorkspace
        Dim session As New NotesSession
        Dim db As NotesDatabase        
        Dim doc As NotesDocument
        Dim vRes As Variant
        
        vRes =  Evaluate({@ClientType})
        Print "Client Type before: " & Cstr(vRes(0))
        
        Set db = session.CurrentDatabase
        Set doc = db.CreateDocument
        Call doc.ConvertToMIME()
        
        vRes =  Evaluate({@ClientType})
        Print "Client Type after: " & Cstr(vRes(0))

 

The Law of Unintended Consequences

1. Oktober 2014 Posted by Björn Großewinkelmann

 

Ok, so we all know that the road to becoming a Notes developer is a rocky one but there really are some things that take the cake in that regard. Today I like to talk about standard functions and that they never behave quite like you might expect them to.

Since I work primarily with LotusScript let us take a look at a very useful und often used function in the NotesDocument class, namely ComputeWithForm.

First let’s read the manual (8.5.3):


ComputeWithForm method

Validates a document by executing the default value, translation, and validation formulas, if any are defined in the document form.

Defined in

NotesDocument

Syntax

flag = notesDocument.ComputeWithForm( doDataTypes, raiseError )

Parameters

doDataTypes

Boolean. The method ignores this parameter. Specify either True or False.

raiseError

Boolean. If True, an error is raised if the validation fails. If False, no error is raised; instead, the method returns False if validation fails.

Return value

  • True indicates that there are no errors on the document.

  • False indicates that there are errors on the document.

[*snip*]



Source: http://www-01.ibm.com/support/knowledgecenter/SSVRGU_8.5.3/com.ibm.designer.domino.main.doc/H_COMPUTEWITHFORM_METHOD.html?lang=en

 

Let us just ignore interesting tidbits like “The method ignores this parameter. Specify either True or False.” and move on to the really strange stuff. So what does this method claim to do?

1) It executes validation formulas

If you work like me you will never use that feature since I always put field validations in my backend class for the following reasons:

  1. I always know where to find them, trust me that is not a small issue when it comes to complex business logic and interdependent rules.
  2. It is far easier to write such complex rules in LS than in Formula Language.
  3. You can debug LS!
  4. You can evaluate all of the rules and give the user a summary report instead of raising an error after the first evaluated rule fails to validate.

 

2) It executes Input Translation Formulas

Yep it does and you have to be aware of that. But if you use Input Translation at all you probably know what you are doing since otherwise you couldn’t.

 

3) It “executes” default values.

It does. Which is the reason why we use it so frequently so we can set all the items we need on a document that was created through backend classes or agents. One ComputeWithForm and all the default values are there. Immensely useful but Notes wouldn’t be Notes if it hadn’t some surprises in stock for us there.

 

4) It returns a Boolean to indicate whether or not the validation succeeded

Well, yes it does …kind of… maybe … if nothing else did go wrong you have no idea about….

 


based on © Andy Dean - Fotolia.com

“Default” is a just a word…

So let us unpack the more problematic und not very well documented “features” of this function and let’s start with the default values it is supposed to execute.

First if you think that “default values” refer to… well default values you are obviously new at this but don’t despair you’ll get there eventually. So no, “executing default values” does NOT ONLY mean that an item is created on the document which has the values you can specify in the “Default”-section of any of the editable fields on a given form. In fact it means a lot more than that.

ComputeWithForm also executes formulas in Computed- and ComputedWhenComposed-Fields. Which is great! But: if no “default” value is available the created item will be of type text (1280) and the value will be set to the empty string! So, arguably yes it does execute default values… if you use that term rather loosely.

… “Error” on the other hand…

If you read the description in the manual and my remarks above you might think that ComputeWithForm essentially creates all not yet existing items then executes the input translations to finalize their values before it checks those values against the validation rules. At this point the function returns either ‘true’ or ‘false’ (or raises an error if you set raiseError = ‘true’) to indicate whether or not all rules validated successfully.

But then I guess at this point you already know better than that.

Suppose you have a DBLookup in one of your computed fields and that lookup fails for whatever reason.
Right, it is an error but is it a validation error?
Even if there is no input validation specified for that field?
And what if you use [FailSilent]?
Still an error?
Surely not and that can’t possibly be treated as a validation error right!?
Well, wrong on all counts!

The type of error does not matter at all. Every single error in a formula on your document will cause ComputeWithForm to fail and to fail miserably! Because every single error, even the ones that aren’t (because of [FailSilent]), will be treated as fatal which means that ComputeWithForm will stop evaluating the document which in turn means that not all the default values will be set. One failed lookup and not another item will be evaluated!

Yes the manual states: False indicates that there are errors on the document.

But I bet you didn’t think they literally meant every possible error. And on top of that even the ones that are already handled!

Well now you know!

So if you for example use a lookup in a computed field that might fail you have to identify why that could possibly be and then check for those conditions with @If. Then ComputeWithForm will finally work as you expected it to all along...

... kind of. At least, if you know what it does and doesn't do you can work around it and might even find it useful... maybe. Of course ComputeWithForm is not the only example of ... interesting ... behavior, it is probably not even the worst one. But from what you can gather on the net a lot of people believe that it works in a lot of different ways that seem to be mutual exclusive and people are (as always) not shy about sharing incomplete information. Which, in a sense, makes this function the worst of its kind after all because people get frustrated.

But maybe I am wrong, maybe I should see stuff like this more relaxed and treat the documentation more like loose guidelines.... What do you think? And while we're at it: What do you believe to be the most atrociously under-documented or reality-bending, may-work-as-intended-on-the-fourth-Tuesday-in-February-on-a-parallel-plane-of-existence-if-it’s-not-a-leap-year-function in the Notes/Domino environment? Leave a comment and share the pain. Trust me, it's good therapy.

 

Wie spreche ich Java-Klassen mit LotusScript an? – LS2J und seine Tücken

1. Oktober 2014 Posted by Christian Annawald

 

Uns stellte sich schon das ein oder andere Mal die Frage, ob es möglich sei, die Programmlogik für XPages und Legacy Anwendungen gemeinsam zu pflegen und zu nutzen. Hierzu drängte sich uns direkt LS2J von IBM auf, ein Framework, das es ermöglicht, aus LotusScript heraus direkt Java-Klassen zu benutzen.

Hier bekommt ihr einen kurzen Überblick zu LS2J: http://www-01.ibm.com/support/knowledgecenter/SSVRGU_9.0.1/com.ibm.designer.domino.main.doc/LSAZ_LS2J_CONNECTING_WITH_JAVA.html

 

Grundsätzlich ist es recht einfach zu benutzen: Als erstes legt man sich eine Java-Klasse an z.B.:

public class myTestClass {

  public void doSomething() {

   System.out.println("Hallo Welt");

  }

 }

 

Als nächstes legt man sich ein wenig LotusScript Code an

Option Declare

 UseLSX "*javacon"

 Use "TestJavaLib"

Sub useJavaClass

      Dim js As New JavaSession

      Dim myClass As JavaClass

      Dim myObject As JavaObject

      Set myClass = js.GetClass("myTestClass")

      Set myObject = myClass.CreateObject

      Call myObject.doSomething()

End Sub

 

Nun fügt man den LotusScript-Teil z.B. in einen Button auf einer Form ein:

  

 

Durch das Benutzen des Buttons erhält man nun in der Java-Konsole folgende Ausgabe:

 

Wer ein weiterführendes Beispiel benötigt, wird hier fündig: http://notes.helsinki.fi/help/help8_designer.nsf/f4b82fbb75e942a6852566ac0037f284/236f3b6a4a38a1028525731b0049a6f0?OpenDocument

 

Leider ist es nur möglich, primitive Datentypen wie int, double, String, usw. zu übergeben - keine Objekte. Und das bringt uns leider zu unserem ersten Problem:

„Wie bekommen wir es hin, ein Notesdokument an eine Java Klasse zu übergeben?“

Jetzt wird jeder sagen „Ganz einfach: Übergib doch die Note ID und hol dir das Dokument aus der Datenbank“. Den Gedankengang hatten wir auch, leider hat sich hieraus ein neues, viel schwerwiegenderes Problem ergeben:

Versucht man, über die bekannten Wege an eine Session zu kommen, wird man kläglich scheitern.

 

Hier mal die Wege, die wir getestet haben:

Versuch 1:

  public class GetSessionV1 {

      public GetSessionV1() {

            NotesThread.sinitThread();

            Session session = NotesFactory.createSession();

      }

}

Resultat: "NotesException: Cannot create a session from an agent"

 

Versuch 2:

  public class GetSessionV2 extends AgentBase{

      public GetSessionV2() {

            Session s= getSession();

     }

}

Resultat: "Session ist null"

 

Den Weg über die DominoUtils haben wir außen vor gelassen, da wir ja nicht auf einer XPage unterwegs sind. Da wir also bis jetzt leider keine Möglichkeit gefunden haben, aus Java heraus das Lotus Notes Backend anzusprechen, ist unsere Idee erstmal auf Eis gelegt. Schade eigentlich.

Vielleicht habt ihr ja eine Idee zu dem Thema?

 

Quick Tip: Benutzervalidierung durch erneute Passwort-Eingabe

30. September 2014 Posted by Thomas Bahn

Quick-TippIBM Notes
Ich hatte letzte Woche eine "kleine" Anforderung von einem Kunden: Er ist interessiert an der erweiterten kommerziellen Version unseres assono Password-Safes. Aber seine interne Revisionsabteilung hatte noch eine neue Anforderung: Der aktuelle Benutzer sollte vor dem Öffnen eines Dokuments oder beim Kopieren eines Passworts in die Zwischenablage direkt aus einer Ansicht vorher noch einmal sein Notes-Passwort eingeben müssen.

Dafür gibt es ein @Command: ToolsUserLogoff. Wenn es ausgeführt wird, logt es den Benutzer aus. Wenn man dieses Kommando mit etwas Code kombiniert, mit dem man auf den Server zugreift, erscheint der Passworteingabe-Dialog, wie es zum Bespiel hier beschrieben wurde: Forcing user re-entry of passwords for electronic signatures in script.

Dieser Ansatz hat aber für mich drei Nachteile:
1. Er funktioniert nicht offline, also z. B. bei einer lokalen Replik, weil der Serverzugriff notwendig ist, um den Passworteingabe-Dialog zu öffnen.
2. Ich möchte es nutzen, um das Öffnen von existierenden Dokumenten abzusichern. Wenn der Benutzer den Dialog abbricht, wird das Dokument trotzdem geöffnet.
3. Der Formel-Code muss im QueryOpen-Ereignis der Maske eingetragen werden. Dort brauche ich aber zwangsläufig LotusScript.

Also musste ich weiter suchen nach einer Lösung in LotusScript. Und ich wurde fündig bei Eknori, der in 2004 diesen Blog-Eintrag verfasst hat: @Command(ToolsUserLogoff) in Lotus Script.
Diese Lösung muss für die aktuellen Versionen von Notes angepasst werden, weil man sich jetzt nicht mehr mit F5, sondern mit Strg-F5 auslogt.

Ich war immer noch nicht überzeugt, dass dies die bestmögliche Lösung für mein Problem sein sollte. Es ist wegen der Verwendung von Windows-DLLs nicht auf andere Plattformen übertragbar, der Benutzer bleibt ausgeloggt, wenn er den Passworteingabe-Dialog abbricht, es würde wohl auch nicht lokal funktionieren usw.

Ich suchte weiter und fand diese großartige Idee: Mittels Notes C-API auf den privaten Teil der Benutzer-ID-Datei zugreifen, genauer mit der  REGGetIDInfoString-Function mit REGIDGetPrivateKey als infoType.
  • Dies würde den Passworteingabe-Dialog erzwingen, aber gleichzeitig den Benutzer nicht abmelden.
  • Es würde auch lokal ohne jeden Server-Kontakt funktionieren.
  • Und ich könnte sogar feststellen, wenn der Benutzer den Dialog abgebrochen hätte und darauf falls nötig reagieren.

Vielen Dank an Davy Vanherbergen für seinen OpenNTF Code Bin-Beitrag (von 2003!): Call notes password prompt from lotusscript 

Ich habe seine Idee genommen und auf "meine Art" neu implementiert. Dabei habe ich unsere C-API-Hilfsfunktionen genutzt, und ich lasse den Benutzer eine andere ID-Datei auswählen, wenn die in der notes.ini eingestellte nicht die seine ist.

Function ValidateCurrentUser As Boolean
        '/**
        ' * validates current user by letting him enter his password
        ' *
        ' * @return  True, if user has successfully entered his password
        ' *
        ' * @author  Thomas Bahn/assono <tbahn@assono.de>
        ' * @version 2014-09-30
        ' */

        Const MAXOUTBUFRLEN% = 4096
       
        Dim idFileName As String
        Dim returnCode As Integer
        Dim userNameBuffer As String*MAXUSERNAME
        Dim actualLen As Long
        Dim currentUserName As String                
        Dim outBufrLen As String*MAXOUTBUFRLEN
       
        If Not IsDebugMode() Then On Error GoTo errorHandler
       
        ValidateCurrentUser = False
       
        idFileName = session.GetEnvironmentString("KeyFileName", True)
        returnCode = REGGetIDInfoString(idFileName, REGIDGetName, _
                userNameBuffer, MAXUSERNAME, actualLen)
        Call ShowCAPIErrorIfAnyAndEnd(returnCode, "REGGetIDInfo", _
                NULLHANDLE)
       
        currentUserName = Left(userNameBuffer, actualLen - 1)        
        Do While session.UserName <> currentUserName
                ' ID file configured in notes.ini is not the ID file of the
                ' current user
                idFileName = uiws.OpenFileDialog(False, _
                        "Wählen Sie Ihre ID-Datei:", "*.ID|", _
                        GetNotesDataDirectory(), idFileName)(0)

                returnCode = REGGetIDInfoString(idFileName, REGIDGetName, _
                        userNameBuffer, MAXUSERNAME, actualLen)
                Call ShowCAPIErrorIfAnyAndEnd(returnCode, "REGGetIDInfo", _
                        NULLHANDLE)
               
                currentUserName = Left(userNameBuffer, actualLen - 1)
        Loop
       
        returnCode = REGGetIDInfoString(idFileName, REGIDGetPrivateKey, _
                        outBufrLen, MAXOUTBUFRLEN, actualLen)
        If returnCode = -32355 Then
                Exit Function ' user cancelled dialog
        Else
                Call ShowCAPIErrorIfAnyAndEnd(returnCode, "REGGetIDInfo", _
                        NULLHANDLE)
        End If

        ' when we get here, the user must have entered his password
        ' successfully
        ValidateCurrentUser = True
        Exit Function
       
errorHandler:
        If HandleError() = RESUME_NEXT_LINE Then Resume Next
        Exit Function
End Function

GetNotesDataDirectory() ist eine Hilfsfunktion, die das Notes-Datenverzeichnis zurück gibt. Ersetze diese Funktion durch deine eigene oder einfach eine String-Konstante.
Und passe den Fehlerbehandlungscode (IsDebugMode() and HandleError()) entsprechend deinen Standards an.

Ich benötige noch einige Deklarationen (Declarations):

Private Const LIBRARY = "Eintrag utils"

' WORD LNPUBLIC OSLoadString(HMODULE hModule, STATUS StringCode, char far *retBuffer, WORD BufferLength);
Declare Function OSLoadString Lib "nnotes" Alias "OSLoadString" (ByVal hModule As Long, ByVal stringCode As Integer, ByVal retBuffer As LMBCS String, ByVal bufferLength As Integer) As Integer

' STATUS LNPUBLIC REGGetIDInfo(char far *IDFileName, WORD InfoType, void far *OutBufr, WORD OutBufrLen, WORD far *ActualLen);
Declare Function REGGetIDInfoString  Lib "nnotes" Alias "REGGetIDInfo" (ByVal idFileName As String, ByVal infoType As Integer, ByVal outBufr As String, ByVal outBufrLen As Integer, actualLen As Long) As Integer
Declare Function REGGetIDInfoBoolean Lib "nnotes" Alias "REGGetIDInfo" (ByVal idFileName As String, ByVal infoType As Integer, ByVal outBufr As Long,   ByVal outBufrLen As Integer, actualLen As Long) As Integer

Const REGIDGetName = 7 ' Data structure returned Is char xx[MAXUSERNAME]
Const REGIDGetPrivateKey = 9 ' Data structure returned Is char xx[xx]


' STATUS LNPUBLIC NSFDbClose(DBHANDLE hDB);
Declare Function NSFDbClose Lib "nnotes.dll" (ByVal hDB As Long) As Integer


Const NOERROR = 0

Const NULLHANDLE = 0&

Const MAXUSERNAME = 256


And two support functions for the C API error handling:

Sub ShowCAPIErrorIfAnyAndEnd(errorCode As Integer, functionName As String, hDB As Long)
        '/**
        ' * shows user the C API error and aborts execution.
        ' *
        ' * @param   errorCode return code of the function's execution
        ' * @param   functionName name of the C API function called
        ' * @param   hDB handle to the open database
        ' *
        ' * @author  Thomas Bahn/assono <tbahn@assono.de>
        ' * @version 2014-07-17
        ' */                
       
        If errorCode = NOERROR Then Exit Sub ' exit if no error occured
       
        If hDB <> 0 Then
                ' if there is a valid handle, try to close database
                Call NSFDbClose(hDB)
        End If
       
        Error Err, "Fehler in Bibliothek '" & LIBRARY & "'" & Chr$(10) & _
        "Ein Fehler ist aufgetreten in der C-API-Funktion'" & _
        functionName & "': "  & Chr$(10) &_
        "Fehler-Code: " & Trim$(Str$(errorCode)) & Chr$(10) & _
        "Fehler-Text: " & Chr$(10) & GetCAPIErrorMsg(errorCode)
End Sub

Function GetCAPIErrorMsg(errorCode As Integer) As String
        '/**
        ' * gets error message for the C API error.
        ' *
        ' * @param   errorCode return code of the function's execution
        ' * @return  error message for the C API error
        ' *
        ' * @author  Thomas Bahn/assono <tbahn@assono.de>
        ' * @version 2014-07-17
        ' */        
       
        Dim length As Integer
        Dim buffer As String
       
        ' initialize a buffer of adequate length to accept the error string
        buffer = String$(256, 0)
       
        ' get the API error message from the internal Notes/Domino string
        ' tables
        length = OSLoadString(NULLHANDLE, errorCode, buffer, Len(buffer))
        If length > 0 Then
                ' remove any trailing characters from the string and
                ' return it to the caller
                GetCAPIErrorMsg = Left$(buffer, InStr(1,buffer,Chr$(0))-1)
        Else
                ' couldn’t locate the error message in the string tables
                GetCAPIErrorMsg = "Unbekannter Fehler"
        End If
End Function

Schließlich platziere Code ähnlich dem folgenden in das QueryOpen-Ereignis deiner Maske:

If continue Then
        continue = ValidateCurrentUser()
       
        If continue Then
                ' do some stuff if necessary
        End If
End If

Da der Code ausschließlich Notes C-API-Aufrufe benutzt, kann er leicht auf weitere Plattformen erweitert werden. Momentan ist er auf Windows beschränkt.

Deutsche Notes/Domino Versionen schreiben falsche Log-Dokumente

26. März 2014 Posted by Manfred Meise

Domino Entwickler verwenden gelegentlich die LotusScript-Klasse "NotesLog" um z.B. mit der "LogAction" Methode in eine entsprechende Datenbank zu schreiben. Hierbei schreibt jedoch ein Notes/Domino Release 9.0.1 Client einen falschen Wert ("Aktion" statt "Action") in das Feld "A$LOGTYPE" der Protokolldokumente.

Das Ergebnis sieht dann wie folgt aus:

Image:Deutsche Notes/Domino Versionen schreiben falsche Log-Dokumente

Durch Umstellen z.B. des Clients auf die englische Benutzeroberfläche werden LogDokumente sauber generiert und angezeigt:

Image:Deutsche Notes/Domino Versionen schreiben falsche Log-Dokumente

Deutsche Notes/Domino Versionen schreiben falsche Log-Dokumente

26. März 2014 Posted by Manfred Meise

Domino Entwickler verwenden gelegentlich die LotusScript-Klasse "NotesLog" um z.B. mit der "LogAction" Methode in eine entsprechende Datenbank zu schreiben. Hierbei schreibt jedoch ein Notes/Domino Release 9.0.1 Client einen falschen Wert ("Aktion" statt "Action") in das Feld "A$LOGTYPE" der Protokolldokumente.

Das Ergebnis sieht dann wie folgt aus:

Image:Deutsche Notes/Domino Versionen schreiben falsche Log-Dokumente

Durch Umstellen z.B. des Clients auf die englische Benutzeroberfläche werden LogDokumente sauber generiert und angezeigt:

Image:Deutsche Notes/Domino Versionen schreiben falsche Log-Dokumente

Deutsche Notes/Domino Versionen schreiben falsche Log-Dokumente

26. März 2014 Posted by Manfred Meise

Domino Entwickler verwenden gelegentlich die LotusScript-Klasse "NotesLog" um z.B. mit der "LogAction" Methode in eine entsprechende Datenbank zu schreiben. Hierbei schreibt jedoch ein Notes/Domino Release 9.0.1 Client einen falschen Wert ("Aktion" statt "Action") in das Feld "A$LOGTYPE" der Protokolldokumente.

Das Ergebnis sieht dann wie folgt aus:

Image:Deutsche Notes/Domino Versionen schreiben falsche Log-Dokumente

Durch Umstellen z.B. des Clients auf die englische Benutzeroberfläche werden LogDokumente sauber generiert und angezeigt:

Image:Deutsche Notes/Domino Versionen schreiben falsche Log-Dokumente

Deutsche Notes/Domino Versionen schreiben falsche Log-Dokumente

26. März 2014 Posted by Manfred Meise

Domino Entwickler verwenden gelegentlich die LotusScript-Klasse "NotesLog" um z.B. mit der "LogAction" Methode in eine entsprechende Datenbank zu schreiben. Hierbei schreibt jedoch ein Notes/Domino Release 9.0.1 Client einen falschen Wert ("Akton" statt "Action") in das Feld "A$LOGTYPE" der Protokolldokumente.

Das Ergebnis sieht dann wie folgt aus:

Image:Deutsche Notes/Domino Versionen schreiben falsche Log-Dokumente

Durch Umstellen z.B. des Clients auf die englische Benutzeroberfläche werden LogDokumente sauber generiert und angezeigt:

Image:Deutsche Notes/Domino Versionen schreiben falsche Log-Dokumente