Lotus Domino cannot move subdocuments from one document to another? - lotus-notes

Our Domino developer told us that it is "technologically impossible" to move subdocuments between documents. Is this true?
Earlier this year he wrote for us a course registration system with the following database diagram:
Now we asked him how to move waitlisted registrants from the full training sessions to those sessions that are not. He said it is impossible. He said we need to re-enter (recreate, copy and paste manually) the waitlist records because Domino cannot move attendees from one session to another.
We have over 1000 attendees in our waitlists.
Is he correct? Is this for real? We hope for a solution.

How to do it depends on the way the documents are linked. But in any case it should be possible to relink the documents using code (formula/lotusscript/java).
The help of the Lotus designer contains a lot of information about application development. Another resource is IBM developerworks
There are numerous Lotus related blogs
From the Lotus Designer help:
MakeResponse: Makes one document a response to another document. The two documents must be in the same database.
Dim session As New NotesSession
Dim db As NotesDatabase
Dim view As NotesView
Dim docA As NotesDocument
Dim docB As NotesDocument
Set db = session.CurrentDatabase
Set view = db.GetView( "All documents" )
Set docA = view.GetFirstDocument
Set docB = view.GetNextDocument( docA )
Call docB.MakeResponse( docA )
docB.Form = "Response"
Call docB.Save( True, True )

There are 2 ways that documents can be linked:
- via keys, the soft way
- hierarchically, using a document-response link (i.e. parent-child)
If there is only the logical link, using keys, you only have to adapt the key fields. If there is a "physical", document-response link, you can easily break and recreate that link. In LotusScript there is the NotesDocument.MakeResponse method to attach any document to a new parent. If both methods are used, redundant of course but practical when you need to recover some links, you need to do both changes. Usually, some key fields are repeated from parent to child
Just for testing purposes, you could try this:
- select a response document that you want to hang elsewhere
- Ctrl-X
- select the new parent document
- Ctrl-V
Do this in a test database, for the key fields won't be updated automatically. By the way: code can be written to repair the keys after pasting such a response document.

If you want to keep existing documents you could programmatically copy/duplicate them.
It's quite easy using copyAllItems method.
Look Domino help (with example) here
You could iterate documents with notesView object (getFirstDocument() / getNextDocument() methods), iterate responses on notesdocument.responses method...
Trust us, it's possible, Domino is flexible :-)

There are basically 2 ways this data is linked in the data model you describe. If the data is linked through response document hierarchies it will be slightly different from key based document structures.
Show this to your developer and, he should literally be able to plug in the code to enable the "move attendees" requirements that you're talking about.
A few things to note.
I am assuming that all the data you need to manipulate is in one database.
I am taking your diagram you've provided literally.
For key based document structures, you'll need to check the view and key values used o find attendee documents. Specifically check these 2 lines in the "MoveAttendeesKeyBased" sub:
Set vwAttendeesByCourseID = db.GetView("(LkupAllAttendeesByCourseID)")
Set dcAttendees = vwAttendeesbyCourseID.GetAllDocumentsByKey(docCourseFrom.CourseID(0), True)
The code is designed to look at a field called "CourseID", "Status" and the value of "Wait Listed" for the status value of attendees to be moved.
It took about 20 minutes to write both versions of this function.
For response based document structures
Sub MoveAttendeesResponseBased(docCourseFrom As notesDocument, docCourseTo As NotesDocument)
%REM
A simple move attendees function if the relationship between courses and attendees is based on
response document hierarchies
%END REM
On Error Goto errHandle
Dim dcAttendees As notesDocumentCollection
Dim docAttendee As notesDocument
Dim iAvailablePlaces As Integer
Dim bMoved As Boolean
If Not (docCourseFrom Is Nothing Or docCourseTo Is Nothing) Then
iAvailablePlaces = docCourseTo.availablePlaces(0)
If 0 < iAvailablePlaces Then
bMoved = False
Set dcAttendees = docCourseFrom.Responses
Set docAttendee = dcAttendees.GetFirstDocument
While Not docAttendee Is Nothing And 0 < iAvailablePlaces
If Ucase(Trim(docAttendee.Status(0)))= "WAIT LISTED" Then
Call docAttendee.MakeResponse(docCourseTo)
If docAttendee.Save(True,True) Then
iAvailablePlaces = iAvailablePlaces - 1
bMoved = True
End If
End If
Set docAttendee = dcAttendees.GetNextDocument(docAttendee)
Wend
If bMoved Then
docCourseTo.availablePlaces = iAvailablePlaces
Call docCourseTo.Save(True,False)
End If
End If
End If
Exit Sub
errHandle:
Messagebox Lsi_info(2) + " - " + Str(Err) + " : " + Error(Err) + ", at line " + Str(Erl)
Exit Sub
End Sub
For key based document structures
Sub MoveAttendeesKeyBased(docCourseFrom As notesDocument, docCourseTo As notesDocument)
%REM
A simple move attendees function if the relationship between courses and attendees uses
(non-response) key based documents
%END REM
On Error Goto errHandle
Dim session As New notesSession
Dim dcAttendees As notesDocumentCollection
Dim docAttendee As notesDocument
Dim iAvailablePlaces As Integer
Dim bMoved As Boolean
' a view that lists attendees by Course ID
Dim vwAttendeesByCourseID As notesView
Dim db As notesDatabase
If Not (docCourseFrom Is Nothing Or docCourseTo Is Nothing) Then
iAvailablePlaces = docCourseTo.availablePlaces(0)
If 0 < iAvailablePlaces Then
Set db = session.CurrentDatabase
' do a lookup of all attendees based on the CourseFrom document course id
Set vwAttendeesByCourseID = db.GetView("(LkupAllAttendeesByCourseID)")
' this is the collection of all attendees under the CourseFrom document
Set dcAttendees = vwAttendeesbyCourseID.GetAllDocumentsByKey(docCourseFrom.CourseID(0), True)
bMoved = False
Set docAttendee = dcAttendees.GetFirstDocument
' While there are attendee documents to process and there are available places to goto
While Not docAttendee Is Nothing And 0 < iAvailablePlaces
' if the attendee's status is "Wait Listed" then move them
If Ucase(Trim(docAttendee.Status(0)))= "WAIT LISTED" Then
' Update the course ID for the Attendee
docAttendee.CourseID = docCourseTo.CourseID(0)
If docAttendee.Save(True,True) Then
' decrement the available places
iAvailablePlaces = iAvailablePlaces - 1
bMoved = True
End If
End If
Set docAttendee = dcAttendees.GetNextDocument(docAttendee)
Wend
If bMoved Then
' available places may be >= 0. Just update the available places so you don't over book the course
docCourseTo.availablePlaces = iAvailablePlaces
Call docCourseTo.Save(True,False)
End If
End If
End If
Exit Sub
errHandle:
Messagebox Lsi_info(2) + " - " + Str(Err) + " : " + Error(Err) + ", at line " + Str(Erl)
Exit Sub
End Sub
Key based documents are a little more work but I think are a better structure because you can easily move documents in databases and restore from backups, copy and paste. With response documents you may have issues restoring backups becasue response documents use parent document UNID's to associate themselves to, and also if you move an attendee by accident, it would be impossible to know which course to put the attendee back onto without the original course information, thus leading you back to a key based structure for documents.(But that's just my opinion).....

Related

How to filter selection document to prevent user from select different document based on field "status"

Before this I ask this question and #Torsten Link suggest that I filter document to prevent user from select different document. Basically I have view, and in this view I have list of document sort by Faulty Status which I set as PFStatus. So I have three status which is Obsolete, Spoilt, and Not Found. So I want to filter so that user choose either these three status only and cannot be mixed up.
So I try to filter using below code but nothing happened.
Set doc = dc.GetFirstDocument()
If (doc.PFStatus(0) = "Obsolete" And doc.PFStatus(0) = "Spoilt" And doc.PFStatus(0) = "Not Found") Then
Messagebox"Please choose either one Write Off selection!"
Exit Sub
Elseif (doc.PFStatus(0) = "Obsolete" And doc.PFStatus(0) = "Spoilt") Then
Msgbox"Please choose only one Write Off selection!"
Exit Sub
Elseif (doc.PFStatus(0) = "Obsolete" And doc.PFStatus(0) = "Not Found") Then
Msgbox"Please choose only one Write Off selection!"
Exit Sub
Elseif (doc.PFStatus(0) = "Spoilt" And doc.PFStatus(0) = "Not Found") Then
Msgbox"Please choose only one Write Off selection!"
Exit Sub
Else
'Some code...
End If
So how can I filter selection of documents? Did I put the code in wrong way? Any help I really appreciate. Thank you. :)
Update question
Below are my view name "WriteOff". And I have a button to create new batch. So I want to try prevent user from create a batch with mixed up Faulty Status.
I created a sample on how to do this
Best is not to put your code in the button, but to create an agent to put your code in. Like this, you don't need to refresh your view while debugging your code.
Set 'Agent list selection' as trigger and Target = None.
Create a button in the view using the following formula (replace 'batch process' by your agent name):
#Command([ToolsRunMacro];"(batch process)")
Here's an example of the agent code on how you can check if pfstatus in selected docs is the same.
Option Public
Option Declare
Sub Initialize
Dim col As NotesDocumentCollection
Dim doc As NotesDocument
Dim vwUI As NotesUIView
Dim ws As New NotesUIWorkspace
Dim session As New NotesSession
Dim dbcurrent As NotesDatabase
Set dbCurrent = session.currentdatabase
'Use vwui.documents to keep documents selected if the agent runs.
'Like this, a user can deselect a faulty document.
'Don't forget to deselect all docs at the end of your code
Set vwui = ws.Currentview
Set col = vwui.Documents
'If a user did not 'select' a document (eg V marker before the docline), but merely positioned on a document,
'you need to create a single doc collection based on the caretnoteid (= id of selected document)
If col.count = 0 And vwui.caretnoteid <> "" Then
Set doc = dbCurrent.Getdocumentbyid(vwui.caretnoteid)
Set col = dbCurrent.createdocumentcollection()
Call col.Adddocument(doc)
End If
'Get status from first document to get status to compare against
Dim statusRef As String
Set doc = col.getfirstdocument
If doc Is Nothing Then Exit Sub 'avoid error when no doc is selected
statusRef = doc.pfStatus(0)
'loop other selected documents to compare status
Set doc = col.getNextDocument(doc)
While Not doc Is Nothing
If doc.pfStatus(0) <> statusRef Then
'A document with another status is selected, so do not continue
Msgbox"Please choose only one Write Off selection!"
Exit sub
End If
Set doc = col.getNextDocument(doc)
Wend
'If code gets here, you can loop all documents again to do you batch processing
'Reset doc to first doc in selected collection
Set doc = col.getfirstdocument()
While Not doc Is Nothing
'... some code to run on current doc in the loop ...
Set doc = col.getNextDocument(doc)
Wend
'Deselect documents at the end of your code
Call vwui.Deselectall()
End Sub
Is PFStatus a multi-value field? If it isn't, it can never have more than 1 value (unless you set more than one value programmatically). Or is it a checkbox field?
I think it would be the best if you simply disallow the selection of documents from multiple categories in the view. See https://www.ibm.com/support/knowledgecenter/en/SSVRGU_9.0.1/basic/H_ONSELECT_EVENT.html
IMHO a status field should never be directly input by the user. You should have buttons that guide the user to perform some functions AND change the status in the meantime.

Document did not loop to find all document for "Active" status

I have a process called PC Inspection. When copy inspection, batch no will be change every time inspection is done. And there is pc inspection form called EmpPCSpec. It will copy data from computer document.
I can create batch but it only created for one document selected only.
Below are the lotusscript code to set new batch and create pc inspection form.
Sub Click(Source As Button)
Dim session As New NotesSession
Dim workspace As New NotesUIWorkspace
Dim db As NotesDatabase
Dim collection As NotesDocumentCollection
Set db = session.CurrentDatabase
Set collection = db.UnprocessedDocuments
Dim ws As New NotesUIWorkspace
Dim uiview As NotesUIView
Set uiview = ws.CurrentView
answer% = Messagebox("Do you want to set batch number?", 4,"Batch Number")
If answer% = 6 Then
InputBatchNo = Inputbox("Please insert the Batch Number. eg : 2014A")
If Not InputBatchNo="" Then
For ii = 1 To collection.count
Set doc = collection.GetNthDocument(ii)
currbatchno = doc.PBatchNo(0)
'--------------------------------------
Gosub SetNewBatchNo
'---------------------------------------
doc.PBatchNo =newbatchno
'------------ set new acceptance form ---------------------------
If doc.PStatus(0) = "Active" Then
Set comdoc = New NotesDocument (db)
comdoc.Form = "EmpPCSpec"
comdoc.ATagNo = doc.PTagNo(0)
comdoc.AYear= Left(InputBatchNo,4)
comdoc.ADept= doc.PDept(0)
comdoc.AUserName= doc.PUserName(0)
comdoc.AUserID= doc.PUserID(0)
comdoc.AUserGroup= doc.PUserGroup(0)
comdoc.ALocation= doc.PLocation(0)
comdoc.AStatus= doc.PStatus(0)
comdoc.ABatchNo=doc.PBatchNo(0)
comdoc.AProcessor= doc.PProcessor(0)
comdoc.ASerialNo= doc.PSerialNo(0)
comdoc.ABrand= doc.PBrand(0)
comdoc.AModel= doc.PModel(0)
comdoc.AType= doc.PType(0)
comdoc.ADisplayUnit= doc.PDisplayUnit(0)
comdoc.ADisplaySize= doc.PDisplaySize(0)
comdoc.ADisplayBrand= doc.PDisplayBrand(0)
comdoc.ARam= doc.PRam(0)
comdoc.AHDD= doc.PHDD(0)
comdoc.AIPAddress= doc.PIPAddress(0)
comdoc.AOperatingSys= doc.POperatingSys(0)
comdoc.ACalLicense= doc.PCalLicense(0)
Call comdoc.ComputeWithForm(False,False)
Call comdoc.save(True,True)
'----------------------------------------------------------------
Call doc.ComputeWithForm(False,False)
Call doc.save(True,True)
End If
Next
Messagebox("Process completed." & Chr(13) & "Press 'F9' to refresh the view.")
Else
Messagebox("Please insert Batch Number.")
End If
End If
Exit Sub
SetNewBatchNo:
currbatchno1 = Strtoken(currbatchno, "-", 1)
If InputBatchNo = currbatchno1 Then
seqno = Strtoken(currbatchno, "-", 2)
newseqno = Val(seqno) +1
newseqno1 = Format(newseqno, "0000")
newbatchno = InputBatchNo + "-" + newseqno1
Else
newbatchno = InputBatchNo + "-" + "0001"
End If
Return
End Sub
How can I get all document where if status is "Active" by looping it? Any help will be appreciated. Thanks!
There are a number of issues with this code: using getNthDocument should be avoided as it is a performance killer and bad practice - at least for larger collections as the code will recalculate the position of the document on every use of getNthDocument. Use getFirstDocument and getNextDocument instead. The same is true for the logic with goto. This is difficult to read, understand and especially to maintain. I suggest you create a logic like this:
Set collection = db.UnprocessedDocuments
set doc = collection.getFirstDocument()
while not doc is nothing
if doc.status(0) = "Active" then
<your logic goes here>
end if
//next doc for processing
set doc=collection.getnextdocument(doc)
wend
Besides the above mentioned issues I think the issue with your code is that you did not establish any sort of looping through all selected documents - getNthDocument() is executed only once, therefore only one document is being touched. But as I already said it is dificult to read and understand when and where the code jumps to during runtime due to the use of goto.

Searching Lotus Notes Databases

I have around 50 lotus notes databases which I need to search through (including RTFs and attachments) based on a rather complicated query (for example: If customer = a or b or c or d or e ... or z & product = x or y) then == match; and a tag to (e.g. tag = found or not found)
However, I wasn't sure the best way to go about this and wanted to check three options.
Using Lotus' 'Search in View' should, after indexing, search all the databases - however I am unsure if it will accept a long, complicated search query
Coding an agent in Lotus SCRIPT to basically perform the search in 1. but this might be a way of getting it to accept a complicated query
Using external software (e.g. X1 Professional Search) to search outside of Lotus Notes (however I am not sure if I will be able to tag the files if I identify them in windows explorer).
Edit: My idea is:
Sub Initialise
Dim session as New NotesSession
Dim db as NotesDatabase
Dim dc as NotesDocumentCollection
Dim doc as NotesDocument
Dim searcy_query$
'On current database
Set db = session.CurrentDatabase
'Force index of database
Call db.UpdateFTIndex(True)
'Construct QUERY
search_query = "(Customer1|Customer2) & (Product1|Product2)"
'Search
Set dc = db.FTSearch(query, 0)
If dc.Count=0 Then
Msgbox "No matches!"
Exit Sub
End If
‘Tag the matched documents with “Flag”
Call dc.StampAll("Flag","Active)")
End Sub
However, I am not sure if this will return all matches and also regarding #TRIM and #UPPER (where to put them into the query as I will be searching all fields and RTFs rather than specific ones)
First of all: Searching more than one database at a time can of course be done "manually" by searching each individual database, collection the results and find a way to present the documents "somehow" (what will not be easy, as documents from different databases cannot be shown in "one" view - you would need to use "shadow- documents" or a web- approach (e.g. XPages))
BUT: Lotus Notes has a built-in function to do this, it is called "Domain Indexer". You can read more about how to setup "domain index" in this IBM Link or in your administration help.
Examples for using the domain- index for multi- database- searches can be found in catalog.nsf in the form "DomainQuery".
The search- string does not have a limit as far as I know, so that you can do very complex searches with this technique and it gives you all the matches.
If you search for a LotusScript- Solution, check the documentation for NotesDatabase.ftdomainsearch for example code like this (taken from developer help):
The following code, when placed in a button on a search form within a directory catalog database, searches the directory for the specified query string, and returns all results.
Sub Click(Source As Button)
Dim s As New NotesSession
Dim db As NotesDatabase
Dim w As New NotesUIWorkspace
Dim uidoc As NotesUIDocument
Dim doc as NotesDocument
Dim quer As String
Dim srdoc as NotesDocument
Set db=s.CurrentDatabase
Set uidoc = w.currentdocument
uidoc.refresh
Set doc=uidoc.Document
quer=doc.query(0)
On Error Resume Next
If db.isopen Then
If Err <> 0 Then
Messagebox STR_DBOPEN_ERROR, 0 , STR_ERROR
Err = 0
Exit Sub
End If
Set srdoc = db.ftdomainsearch(quer, 0)
If Err <> 0 Then
Messagebox STR_NDF , 0 , STR_LOTUS_NOTES
Err=0
Exit Sub
End If
srdoc.Form="SearchResults"
Call w.EditDocument(False, srdoc, True)
End If
End Sub
If you also intend to search Rich Text, the simplier way is Full Text Search, you got the point!
The syntax for your query is:
generic_text or ([_CreationDate]<07.10.2014 and ([CustomerFieldName]="Smith" or [CustomerFieldName]="Wesson"))
For formating the Result, I'ld suggest looking at:AppendDocLink for something like:
Dim session As New NotesSession
Dim db As New NotesDatabase("", "resu.nsf")
Dim newDoc As NotesDocument
Dim rtitem As NotesRichTextItem
Dim dc As NotesDocumentCollection
Dim doc As NotesDocument
Set newDoc = New NotesDocument( db )
Set rtitem = New NotesRichTextItem( newDoc, "Body" )
'you have to loop on your 50 DBs
'Search =>from your code
Set dc = db.FTSearch(query, 0)
while not doc is nothing
Call rtitem.AppendDocLink( doc, db.Title )
Call rtitem.AddTab( 1 )
Call rtitem.AppendText( doc.FieldImportantInResult( 0 ) )
Call rtitem.AddNewLine( 1 )
Set doc = dc.GetNextDocument( doc )
Wend
newDoc.save true, true
'this part is not to add it you plane to run your search in background
Dim w As New NotesUIWorkspace
Call w.EditDocument(False, newDoc, True)

Lotus notes - refering to a doc. from a NotesDocumentCollection

I have a view which on top there is a button called Delete
I want to delete all the selected documents from the view; for this I used:
Dim session As New NotesSession
Dim workspace As New NotesUIWorkspace
Dim database As NotesDatabase
Dim documentCollection As NotesDocumentCollection
Set database=session.CurrentDatabase
Set documentCollection=database.UnprocessedDocuments
If documentCollection.Count=0 Then
Msgbox "No documents selected ",,"warning"
Else
userChoice=Msgbox ("Delete" & Cstr(documentCollection.Count) & " documents?",64+100, _
"Confirm...")
If userChoice=6 Then
Call documentCollection.RemoveAll(True)
Call workspace.ViewRefresh
End if
But, what if I want to delete only some docs ( from all selected docs. from view ) which have let say Value = YES where Value is a text field inside the document?
I tried declaring:
Dim ui As NotesUIDocument
Dim doc As NotesDocument
Set doc=ui.document
But I get the message: Object variable not set. So I guess I have to refer to a document using the NotesDocumentCollection? How?
Thanks for your time!
Your Error Message has nothing to do with your question... The Error Message comes from setting doc form an unitialized uidoc. You need to have a Set ui = ws.CurrentDocumentsomewhere in your code, and of course declare ws: Dim ws as New NotesUIWorkspace
But for your question you don't need an ui- document at all. To delete just some of the selected documents, you cycle through the collection and delete just the documents that match your criteria:
Dim doc as NotesDocument
Dim nextDoc as NotesDocument
Set doc = documentCollection.GetFirstDocument()
While not doc is Nothing
Set nextDoc = documentCollection.GetNextDocument(doc)
if doc.GetItemValue( "Value" )(0) = "Yes" then
call doc.Remove(True)
end if
Set doc = nextDoc
Wend
Or you reduce the collection to just contain the documents that match your criteria and then delete the whole collection:
Call documentCollection.FTSearch("[Value] = Yes",0)
Call documentCollection.RemoveAll()
But take care: The collection is reduced with a FTSearch, that might also get "Yes of course" or "Ye" depending on the setting of the FT Index of the database -> Not very reliable.
You need to loop through the documents in the document collection and then process them individually. Here's an example loop that uses your documentCollection variable:
Dim doc As NotesDocument
Set doc = documentCollection.GetFirstDocument
While Not(doc Is Nothing)
' Do stuff
' Get next document
Set doc = documentCollection.GetNextDocument(doc)
Wend

Retrieving inline images from Lotus notes using lotusscript

I have some NotesDocument where some RichText fields have both text and inline images. I can get text part of that items but can't retrieve inline images using lotusscript. Could any one please suggest me a way to retrieve inline images from that documents.
LotusScript code:
Sub Click(Source As Button)
Dim session As New NotesSession
Dim db As NotesDatabase
Dim mainDoc As NotesDocument
Dim v As NotesView
Set db = session.CurrentDatabase
Dim fileName As String
Dim fileNum As Integer
fileNum% = Freefile()
fileName$ = "D:\data.txt"
Open FileName$ For Append As fileNum%
Set v = db.GetView("MyView")
Set mainDoc = v.GetFirstDocument
While Not ( mainDoc Is Nothing )
Forall i In mainDoc.Items
If i.Type = RICHTEXT Then
Write #fileNum% , i.Name & ":" & i.text 'how the images??
End If
End Forall
Set mainDoc = v.GetNextDocument( mainDoc )
Wend
End Sub
Thanks.
Midas is the easiest way to do it, but it isn't free. (It's more than worth the money in overall time saved, but if your organisation is anything like the ones I've worked for, the entire cost of the tool is going to be foisted off on the billing unit that owns the current project, rather than have it amortized over the entire org, and they're likely to change their requirements before agreeing to the cost.) There is another approach, and that's to export the database to DXL (Domino XML) using the export option ConvertNotesBitmapToGIF. The images will turn up in the XML as <picture> elements with the data Base64-encoded. If you're operating entirely within the Notes environment, you'll need to create a temporary document with a rich text field used as a NotesMIMEEntity to transform the encoded picture to binary before streaming it to a file (using NotesStream). All of this assumes that you are working with version 6 or higher; if you are on R5 or earlier, Midas or directly accessing the CD records using the C API are the only way to fly.
Seven years later, and I've been pulling my hair out over this one. Rod H's answer is for attachments, but embedded images are another thing entirely.
My best luck came from #andre-guirard's LotusScript Gold Collection code located here: https://www.openntf.org/main.nsf/project.xsp?r=project/LotusScript%20Gold%20Collection However, that doesn't get everything because it doesn't handle documents where the embedded images were embedded the old way. (Notes changed the way it stores embedded images.)
I tried very hard to combine it with AGECOM's information presented here: https://www.agecom.com.au/support/agecomkb.nsf/0/58cbf10f0ab723c9ca25803e006c7de8?OpenDocument by changing Andre's EmbeddedImage object to seamlessly handle both formats by looking to see if the embedded image within a rich text field is really just a pointer to a $FILE field and then, if so, getting a FileItem object, but eventually I exhausted my understanding and options to a degree that I couldn't justify spending my employer's resources (my time) on it.
So if you have embedded images that are all contained in the new way, I think Andre's code will work unmolested. Otherwise, I tried my best but I don't have an answer... I have what is (for me) a dead end, presented in the hopes that you or someone else who stumbles upon it can embarrass me by explaining what I was doing wrong!
Basically, I started with Andre's code and changed it in the following ways...
In DOMUtils, add the following method:
%REM
Function DU_GetMeOrNextSiblingWithAttr
Description: Starting with a particular node, return that node or the next sibling with an attribute that has a particular value.
Does not recurse into the tree; looks only at the node passed and later siblings.
Parameters:
nodeStart: node to start your search with.
targetElement: element name of desired node.
attrName: attribute name you want to check.
attrValue: attribute value of element you're looking for.
flags: string-matching flags to compare attribute, e.g. 1 for case insensitive.
%END REM
Function DU_GetMeOrNextSiblingWithAttr(nodeStart As NotesDOMNode, ByVal targetElement$, ByVal attrName$, ByVal attrValue$, ByVal flags%) As NotesDOMElementNode
Dim node As NotesDOMNode, elTmp As NotesDOMElementNode
Set node = nodeStart
Do Until node.Isnull
If node.Nodetype = DOMNODETYPE_ELEMENT_NODE Then
If node.Nodename = targetElement Then
Set elTmp = node
If StrComp(elTmp.Getattribute(attrName), attrValue, flags) = 0 Then
Set DU_GetMeOrNextSiblingWithAttr = elTmp
Exit Function
End If
End If
End If
Set node = node.Nextsibling
Loop
End Function
Replace FileItem.New with the following code:
%REM
Sub New
Description: Arguments are the parsed DOM node of the element representing a
design element, and the name of the composite item you would like to read,
modify or create.
%END REM
Sub New(parent As FileItemParent, elNote As NotesDOMElementNode, itemName$, fileName$)
Set m_elNote = elNote
SetItem elNote, itemName$, fileName$
Dim node As NotesDOMNode
Set node = m_elNote.Parentnode
While node.Nodetype <> DOMNODETYPE_DOCUMENT_NODE
Set node = node.Parentnode
Wend
Set m_domd = node
parent.RegisterFileItem Me ' make sure the design element knows about us.
' (in case someone gets smart and invokes the constructor directly
' instead of using the nice methods we've provided).
End Sub
%REM
Sub SetItem
<!-- Created Dec 6, 2017 by JSmart523 -->
If fileName$ is blank, returns the XPath equivalent of elNote/ancestor::document/item[#name=itemName$][position()=1]
If fileName$ is not blank, returns the XPath equivalent of elNote/ancestor::document/item[#name=itemName$][object/file/#name=fileName$][position()=1]
Case insensitive. Changes itemName$ and fileName$ to the correct case if found.
Also sets Me.m_elItem to the returned NotesDOMElementNode
Also sets Me.m_elRawData to the file contents
%END REM
Sub SetItem(elNote As NotesDOMElementNode, itemName$, fileName$)
Dim elFile As NotesDOMElementNode
Dim node As NotesDOMNode
'set node to ancestor::document
Set node = elNote
Do Until node.NodeName = "document"
Set node = node.ParentNode
Loop
'If fileName$ = "", get the first ancestor::document/item[#name=itemName$]
'Otherwise, get the first ancestor::document/item[#name=itemName$][/object/file/#name=fileName$]
Set m_elItem = DU_GetChildOfType(node, DOMNODETYPE_ELEMENT_NODE)
QualifyingItem m_elItem, itemName$, m_elRawData, fileName$
m_itemName = itemName$
m_fileName = fileName$
End Sub
%REM
Sub QualifyingItem
<!-- Created Dec 8, 2017 by JSmart523 -->
Starting with incoming elItem node, ensures it's an item we want or changes elItem to the first sibling that qualifies.
%END REM
Sub QualifyingItem(elItem As NotesDOMElementNode, itemName$, elRawData As NotesDOMElementNode, fileName$)
Dim elFile As NotesDOMElementNode
Dim node As NotesDOMNode
Dim elObject As NotesDOMElementNode
If Not elItem Is Nothing Then
'Initially, elItem is just a starting point, not necessarily the item we want.
'If it's an item with the right name, great, otherwise change elItem to the next sibling item with the right name.
Set elItem = DU_GetMeOrNextSiblingWithAttr(elItem, "item", "name", itemName$, 1)
If Not elItem Is Nothing Then
If fileName$ = "" Then
'we have the right item, and aren't looking for a file node, which means we want the rawitemdata node
Set elRawData = DU_getChildNamed("rawitemdata", elItem)
Else
'We are looking for a $FILE item that contains a file.
'There are possibly several $FILE items within a document, one for each file. We've got the right one if ./object/file/#name = fileName$
Do
Set elObject = DU_GetChildNamed("object", elItem)
If Not elObject Is Nothing Then
Set elFile = DU_GetChildWithAttr(elObject, "file", "name", fileName$, 1)
If Not elFile Is Nothing Then
'Yay! We have the right elItem node!
Set elRawData = DU_GetChildNamed("filedata", elFile)
fileName$ = elFile.GetAttribute("name")
Exit Do
End If
End If
Set elItem = DU_GetMeOrNextSiblingWithAttr(elItem.NextSibling, "item", "name", itemName$, 1)
Loop Until elItem Is Nothing
'At this point, either we jumped out of the loop with a valid elItem and elRawData, or elItem is Nothing
End If
End If
End If
If elItem Is Nothing Then
'we didn't find the correct item
'make sure elRawData is changed to Nothing, too.
Set elRawData = Nothing
Else
itemName$ = elItem.GetAttribute("name")
End If
End Sub
Also in FileItem script library, add a new class, FileItemParent
%REM
Class FileItemParent
<!-- Created Dec 5, 2017 by JSmart523 -->
This is a base class for objects that use FileItem objects
%END REM
Class FileItemParent
m_elElRoot As NotesDOMElementNode
m_elFD As NotesDOMElementNode
Public m_fileItem As FileItem
m_fItems List As FileItem ' list of FileItems we've created and returned to caller.
m_iMode As Integer
%REM
Property Get DOMElement
Description: Return the element node representing the design element.
%END REM
Public Property Get DOMElement As NotesDOMElementNode
Set DOMElement = m_elElRoot
End Property
%REM
Sub New
Arguments:
db: the database containing the design element.
elElement: the DOM element corresponding to the design note (e.g. the <note>
element).
domp: The DOM parser object containing elElement.
%END REM
Sub New(elElement As NotesDOMElementNode)
Set m_elElRoot = elElement
End Sub
Sub Delete
On Error Resume Next
ForAll thing In m_fItems
Delete thing
End ForAll
End Sub
%REM
Function HasItem
Description: Determine whether there's an item element in the note DXL with a
given item name.
Note that the presence of an item doesn't guarantee it's formatted as a file
CD record.
%END REM
Function HasItem(ByVal itemName$) As Boolean
HasItem = Not (DU_GetChildWithAttr(m_elElRoot, "item", "name", itemName, 1) Is Nothing)
End Function
%REM
Function RegisterFileItem
Description: For internal use -- lets the FileItem class notify us that it's
referencing our DOM tree so that we can delete the object if we erase the
corresponding item element.
%END REM
Sub RegisterFileItem(x As FileItem)
Set m_fItems(LCase(x.itemName)) = x
If m_FileItem Is Nothing Then
Set m_FileItem = x
End If
End Sub
%REM
Function GetFileItem
Description: Retrieve the FileItem object associated with a CD-record item.
An object will be returned even if the item doesn't exist, which you can
use to create the item via UpdateFile method.
%END REM
Function GetFileItem(itemName$, fileName$) As FileItem
Set GetFileItem = New FileItem(Me, m_elElRoot, itemName, fileName)
End Function
End Class
The FileItemParent class is primarily code taken from Andre's FileResource class so that both FileResource and EmbeddedImage can use it. Change FileResource to extend FileItemParent, removing any duplicated code.
Now we want to change EmbeddedImage so that, even if the embedded image node contains a link to a $FILE item rather than the actual contents, return the actual contents.
So, change EmbeddedImage to extend FileItemParent
Add/replace the following methods to EmbededImage
%REM
Sub InitFileItem
<!-- Created Dec 6, 2017 by JSmart523 -->
Called by New
%END REM
Sub InitFileItem()
Dim buffer As Variant 'byte array
Dim iFileNameLen As Integer
Dim sFileName As String
Dim sItemName As String
Dim stream As NotesStream
If Len(m_b64) < 30000 Then
'If content is short then maybe it's a link to a $FILE item instead of the actual content?
Dim session As New NotesSession
Set stream = session.CreateStream()
Base64ToBinary m_b64, stream
stream.Position = 0
buffer = stream.Read(1)
If buffer(0) = 196 Then
'this is a link to a $FILE, not the actual image contents!
stream.Position = 10
buffer = stream.Read(2)
iFileNameLen = ConvertWordByteArray(buffer)
stream.Position = 24
buffer = stream.Read(iFileNameLen)
sFileName = BytesToString(buffer)
sItemName = "$FILE"
GetFileItem sItemName, sFileName 'sets m_fileItem to a FileItem object
End If
End If
End Sub
%REM
Property Get SuggestedFileName
%END REM
Public Property Get SuggestedFileName As String
If m_fileItem Is Nothing Then
SuggestedFileName = "Embedded-" + ItemName + "." + SuggestedFileType
Else
SuggestedFileName = m_fileItem.FileName
If InStr(SuggestedFileName, ".") = 0 Then
SuggestedFileName = SuggestedFileName + "." + SuggestedFileType
End If
End If
End Property
%REM
Property Get SuggestedFileType
%END REM
Public Property Get SuggestedFileType As String
If ImageType = "notesbitmap" Then
SuggestedFileType = "bmp"
Else
SuggestedFileType = ImageType
End If
End Property
%REM
Sub ReadFileToStream
%END REM
Sub ReadFileToStream(streamOut As NotesStream)
If m_FileItem Is Nothing Then
ReadToStream streamOut
Else
Set m_FileItem.Stream = streamOut
m_FileItem.Load
End If
End Sub
and then change EmbeddedItem.New to, at the end, call InitFileItem so that if it's a link then getting the contents returns the contents rather than the link.
Okay, so far so good as far as I know, but the problem is that CD Records of embedded images stored within $FILE items (i.e. the rich text field's embedded image node simply contains a link rather than the actual image) are/were documented in a way that was, for me, impenetrable, despite AGECOM's code and explanations. I could use the above code and Andre's EmbeddedImageList object to grab every single embedded image but I simply couldn't get a "ConvertOldCDToNew" method working so I couldn't convert the old CD Record format into solid, uncorrupted files! I don't know if I was stripping too many bytes, not stripping the right ones, or maybe I just forgot to carry the two!
I suggest you look at the Genii Software MidasLSX product. They offer a package of LotusScript extensions that make it easier to deal with the complexities of Lotus Notes Rich Text items.
http://www.geniisoft.com/showcase.nsf/MidasHelp
Otherwise, you can experiment with the NotesRichTextNavigator class to gain access to the image in the rich text item (in theory). There is very little documentation on this sort of thing. I couldn't quite tell what an image would appear as using that class, but assuming you navigate through the rich text item and are able to get a handle the image as a NotesEmbeddedObject, I know there's a way to save the object to disk from that class.
Another (crazy) thought is to email the document, and have it received by another program that can more easily process the body of the email. Notes just isn't very helpful with processing its own rich text fields.
Here is an agent I use to detach files from a richtext field on my documents.
Option Public
Dim uidoc As notesuidocument
Dim doc As NotesDocument
Dim db As NotesDatabase
Dim obj As NotesEmbeddedObject
Dim collection As NotesDocumentCollection
Dim rt As Variant
Dim attachNames As Variant
Dim i As Integer, x As Integer
Dim j As Integer
' Agent - Detach Attachments to C drive for later reattachment
' Copies all attachment in richtext field to personal directory.
Sub Initialize
Dim ws As New notesuiworkspace
Dim ses As New NotesSession
Set db = ses.CurrentDatabase
Set collection = db.UnprocessedDocuments
' get first doc in collection
For j = 1 To collection.Count
Set doc = collection.GetNthDocument( j )
' --- create array of filenames of all the attachments in the document
i = 0
Redim attachNames(i)
Forall x In doc.items
If x.name = "$FILE" Then
attachNames(i) = x.values(0)
i = i + 1
Redim Preserve attachNames(i)
End If
End Forall
If i > 0 Then
Redim Preserve attachNames(i-1)
End If
' --- for all of the filenames in attachNames, if it exists in the rich text field, detatch them
If doc.hasItem("richtextfieldname") Then
Set rt = doc.GetFirstItem("richtextfieldname")
End If
If attachNames(0) <> "" Then
Forall x In attachNames
Set obj = rt.GetEmbeddedObject( x )
If Not( obj Is Nothing ) Then
Call obj.ExtractFile( "C:\path\" & Cstr(x) )
End If
End Forall
End If
Call doc.save(True, False)
Next
End Sub

Resources