Saturday, February 23, 2008

LotusScript to translate text between languages

I am currently working to update our Sametime Bot with a new feature: translation of text. It will be a prompted flow where user is asked for input language, output language and text. The answer from the bot is the translated text. The translation is done by using a LotusScript agent which gets the translation from Google Translate page. LotusScript agent uses MSXML object for web communication with the Goggle server.
See below for an example of translation between English and German.
Pictures show the messagebox for German, Spanish, French and Russian translations. The translation is of course not perfect, but enough good to understand the general meaning.
The best automated translator I've seen so far is by the company "Prompt": http://www.e-promt.com/ It produces very correct translations and even sees the difference between "hello all Domino developers" and "hello to all Domino developers".


Sub Initialize
Dim xmlhttp, MySoap
Dim StartTag As String,EndTag As String, FromLang As String, ToLang As String, TextToTranslate As String
StartTag= |<div id=result_box dir="ltr">|
EndTag = "</div>"
FromLang="en"
ToLang="de"
TextToTranslate="hello all Domino developers"
TextToTranslate=Replace(TextToTranslate," ","%20")

WebServer="http://translate.google.com/translate_t?text="+TextToTranslate+"&hl=en&langpair="+FromLang+"|"+ToLang+"&ie=utf-8"
Set xmlhttp = CreateObject("MSXML2.ServerXMLHTTP")
Call xmlhttp.open("GET", WebServer, False)
Call xmlhttp.setRequestHeader("Content-Type", "text/html; charset=utf-8")
Call xmlhttp.setRequestHeader("User-Agent","Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)")
Call xmlhttp.setRequestHeader("Accept","image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*")
Call xmlhttp.setRequestHeader("Accept-Language","sv")
Call xmlhttp.setRequestHeader("Content-Type","application/x-www-form-urlencoded")
Call xmlhttp.send(Null)

strxml = xmlhttp.responseText

Msgbox Strleft(Strright(strxml,StartTag),EndTag)

End Sub







Tags:

Sunday, February 03, 2008

Sametime bot to read Lotusphere RSS feeds is updated

Hey folks! I am back from the long period of not blogging! And several more posts are in the pipeline, including DXL API for handling pictures/files, webcam API for spying on Notes users and Java agent to impersonate web users! :)

Last week I've updated my Sametime bot which shows blog posts in Lotusphere2007 category(on Technorati). Now it shows the last 20 posts for keyword Lotusphere2008.
You can try it here: Sametime bot

If you want to see another blog category available through STWidget and Sametime Bot, drop me a line and I'll create a new function and an appropriate STWidget link. Or do you have an idea for a cool Bot function not related to RSS? Drop me a line and I'll see what I can do :)

For sample of LotusScript agent fetching the RSS data from Technorati, see the old post: Sametime bot shows latest Lotusphere2007 blogs

Tags:

Thursday, October 11, 2007

Using regex for matching multiple words anywhere in the sentence

This blog post is to save time for those developers who want to use Regular Expressions to determine whether ALL of the multiple words are located anywhere in the input string. It took me several hours to make it work right for 3 sets with 3 alternative words in each set. Google was not to much help, only a couple of pages contained useful examples.

It began with me implementing regex (regular expressions) matching mechanism for our Sametime Bot to make a more flexible pattern-matching solution than current wildcard matching. So that instead of simply specifying *helpdesk* to match all incoming questions where word "helpdesk" is present, with regex it is possible to fine-tune the match and handle "what is phone number to helpdesk?" incoming question and "How can I contact helpdesk on weekends" question differently. Matching capabilities of regex are amazing, there are very little operations you can't do with it.

Pattern "any one word is enough":
helpdesk|assistance|support

Matches for: Can I get some assistance? How can I contact support? Does helpdesk have an email address?
Not matches for: What's the time? Can you assist me?

-------------------------------------

Pattern "all words must be present":
^(?=.*?(phone|fone|call|contact))(?=.*?(help|assistance|support)).*$

Matches for: What is the phone number to helpdesk? Can I call to support department from my cell phone? How can I contact helpdesk?
Not matches for: I need help! I want to call my mom. My phone doesn't work. Charlie, Charlie, this is Bravo, send more air support!

-------------------------------------

Pattern "all words must be present, but NOT that one":
^(?=.*?(phone|fone|call|contact))(?=.*?(help|assistance|support))((?!weekend|night).)*$

Matches for: I want to come in contact with support now. I need phone assistance to install ABC software today.
Not matches for: What number can i call on weekends to get help with this tool? What phone number can I call to contact helpdesk at night?

-------------------------------------


With a little help from blog post on lekkimworld, I created this LotusScript testing module so the creator of the pattern can test the pattern functionality by providing a text string which is matched to the pre-defined regex expression. The result of the test is either Match or Not match.

Sub Click(Source As Button)
Dim workspace As New NotesUIWorkspace
Dim uidoc As NotesUIDocument
Dim doc As NotesDocument
Set uidoc = workspace.CurrentDocument
Set doc=uidoc.Document
Dim regexp As Variant
Dim result As Integer
Set regexp = CreateObject("VBScript.RegExp")
regexp.IgnoreCase = True
uinput=Inputbox("Input text to test for pattern match:", "Regex tester", userinput)

userinput=uinput
regexp.Pattern = doc.RegmatchSubject(0)
result= regexp.Test(userinput)
If result = -1 Then
Msgbox "Regex match found!"
Else
Msgbox "Regex match NOT found!"
End If
Set regexp=Nothing
End Sub

Online demo of regex questions to Bot: http://www.botstation.com/sametime/bot_regex.html

Sametime Bot regex


Tags:

Wednesday, September 12, 2007

Morse code in @Formula language and as a function in Sametime Bot

Here comes another post about using Lotus Notes' @Formula programming language for accomplishing different tasks. This time I'll show how to convert text to Morse code. Morse code is a method for transmitting telegraphic information, using standardized sequences of short and long elements to represent the letters, numerals, punctuation and special characters of a message.




The @Formula code is rather short. First we by specify 2 arrays: one with letters and another with corresponding morse code. Then we simply use @ReplaceSubstring function to replace the letters in the input text to corresponding elements in the morse array. As the number of elements in the 2 arrays are the same, @ReplaceSubstring function applies 1-to-1 element replacement.

letters:=" ":"A":"B":"C":"D":"E":"F":"G":"H":"I":"J":"K":"L":"M":"N":"O":"P":"Q":"R":"S":"T":"U":"V":"W":"X":"Y":"Z":"1":"2":"3":"4":"5":"6":"7":"8":"9":"0";
morse:=" ":".-":"-...":"-.-.":"-..":".":"..-.":"--.":"....":"..":".---":"-.-":".-..":"--":"-.":"---":".--.":"--.-":".-.":"...":"-":"..-":"...-":".--":"-..-":"-.--":"--..":".----":"..---":"...--":"....-":".....":"-....":"--...":"---..":"----.":"-----";
plaintext:="Morse code";
encoded:=@ReplaceSubstring(@UpperCase(plaintext);letters;morse+" ");
encoded


To reverse the process and convert from morse code to plain text following code can be used:

letters:="A":"B":"C":"D":"E":"F":"G":"H":"I":"J":"K":"L":"M":"N":"O":"P":"Q":"R":"S":"T":"U":"V":"W":"X":"Y":"Z":"1":"2":"3":"4":"5":"6":"7":"8":"9":"0";
morse:=".-":"-...":"-.-.":"-..":".":"..-.":"--.":"....":"..":".---":"-.-":".-..":"--":"-.":"---":".--.":"--.-":".-.":"...":"-":"..-":"...-":".--":"-..-":"-.--":"--..":".----":"..---":"...--":"....-":".....":"-....":"--...":"---..":"----.":"-----";
encoded:="-- --- .-. ... . -.-. --- -.. . ";
plain:=@ReplaceSubstring(@Implode(@Replace(@Explode(@ReplaceSubstring(encoded;" ";" _ ");" ");morse;letters);"");"_";" ");
plain




I also implemented same morse formula as a function in Botstation Bot Framework, where user passes a text string to Sametime bot and receives the morse code as output. Here is a picture and a live example.
Bot command syntax:
morse HERE IS TEXT TO ENCODE
demorse .. .-.. --- ...- . ... .- -- . - .. -- .

Morse Bot online example: http://www.botstation.com/sametime/stwidget_morse.php





Tags:

Monday, September 10, 2007

Remove HTML tags using one line of @Formula

When you need to remove HTML tags from HTML-formatted text, you can use following Domino @Formula:

@ReplaceSubstring(@ReplaceSubstring(OriginalText;"<br>":"<li>":"<ul>":"</ul>";@NewLine:@NewLine:(@NewLine+@NewLine):(@NewLine+@NewLine));"<"+@Right(@Explode(OriginalText;">");"<")+">";"")

See attached picture for example of original HTML-formatted text and of resulting text where HTML tags are stripped out.

The @Formula does following:
1) Splits the original text using "<" as separator, creating an array of strings
2) In each array element takes the text to the right of the ">" character, which gives us every HTML tag used in the original text, for example BR, B, U, LI, FONT
3) Adds "<" and ">" to the calculated tags, so the array now consists of <BR>, <B>, <U>, <LI>, <FONT> etc.
4) In the original text replaces the occurances of computed tags to empty string "", thus stripping HTML out of text.

As we often want to keep line breaks to keep the original look, then before replacing the tags with empty string, we replace <BR>, <UL>, <LI> with a hard new line. Without handling line breaks the code is much shorter:

@ReplaceSubstring(OriginalText;"<"+@Right(@Explode(OriginalText;">");"<")+">";"")



Sunday, June 03, 2007

Prohibit users to trigger agents from web

When developing agents, it's easy to forget that every agent can be triggered from web with agentname?OpenAgent URL. Such agent invocation can cause unpredictable results.
To avoid this, developer has these 2 options:
1) Hide agent from web using property "hide design element from: Web browsers"
2) Programmatically find out if the agent is triggered from web and exit

Sub Initialize
ev=Evaluate("@ClientType")
If ev(0)="Web" Then Exit Sub 'Do not run agent if triggered from web
'here goes the rest of the code
'which will be executed in Notes client but not on web
End Sub

Sunday, May 20, 2007

Domino geek meeting in Stockholm

There will be a meeting in Stockholm (Sweden) on May 24, for people working with Lotus Domino and related software. The meeting is hosted by company Ekakan.
Read more here: http://www.ekakan.com/g33k

Technorati tags:
,

Friday, May 18, 2007

You've got a message... from the Second Life

Nicholas Chase has published an interesting article on IBM Developerworks. It's about making it possible to chat with people in Second Life (SL), without opening SL program. That's a rather innovative solution and I think that many SL users would appreciate such possibility.

It works like this:
1) SL person clicks an object (a picture, a cube) which says "Type message now and it will be send to John Doe"
2)SL person types the message, like "Hi John, can you hear me?", while being near the object.
3)The object can "see" the typed text and sends it to a web servlet using HTTP call. Web server has a connection to Sametime server through Sametime bot.
4) Sametime bot forwards the message to John Doe, who is logged in on Sametime network.
5) John Doe has his Sametime client started and receives a message from Sametime Bot, saying "Hi John, can you hear me?".
6) John Doe types "Yes, how can I help you?" as the response to bot in the Sametime client chat window.
7) Bot outputs the answer to the servlet and the servlet outputs it back to the requesting SL object.
8) The object shows message "Yes, how can I help you?" in SL. Anyone near the object can see the message. So you actually do not chat with the person, but with the object itself, which "broadcasts" your message to all people nearby. I think your response message can in theory also be a private message send only to that person, but the person's request message is always visible to others.

http://www-128.ibm.com/developerworks/edu/ls-dw-ls-stsl.html

I will try to implement the suggested solution and see if it can be used for something more useful than spam-chatting with people near chatboxes :)
Like having a "chat conference meeting" where people do not need to be logged in to SL, and actually do not even need an SL account. Can be interesting solution for chat-only SL meetings hold by companies like IBM. It should be possible to sort away chats from people other than the meeting's chairman. Just imagine an online conference where instead of people's avatars you see a lot of chatting cubes :)

I will also try to connect the solution to my company's Sametime Botand use STWidget as a chat client. Having AJAX/Flash-based STWidget web client as chat interface would eliminate the need to download and install Sametime client.

There are thousands of virtual shops in SL selling virual clothes, virtual furniture and stuff, and some shop owners would probably like to have an option to chat with customers even when they are not logged in to SL. Not sure if there is already some software for this #.

Following things are to consider when developing SL-to-Sametime chat solution for more than 1 user:
* Noone in SL knows what Sametime is and don't care much either. A Sametime-less option would be great (Apache+MySQL+PHP).
* Sametime bot must be hosted somewhere.
* Same Sametime bot should be able to handle tens of thousands of objects and hundreds of simultaneous chats. I guess our Sametime bot can be extended for this purpose, but it's still a difficult task. Probably several bots at several locations would be needed.
* People do not have Sametime client and don't want one. Can be solved with STWidget though.
* People do not have access to Sametime servers. Can be temporary solved by using IBM's demo server.
* The biggest question: will Sametime add too much overhead to the solution, without actually making it easier to develop and maintain? Theoretically a message can be sent directly to STWidget chat client without going through Sametime network.


I'll publish the results of my tests.

Tags:

Monday, April 23, 2007

Run scheduled agent every 60 seconds

The shortest time between executions of a scheduled agent is 5 minutes. But very often you want to run some important function with only 1 minute interval. Your function takes maybe only 2 seconds to run, but you still are required to wait 5 minutes before it can be started again at the next agent invocation.

The agent I created makes it possible to simulate running agent every 60 seconds, or even every 2 seconds if you wish so. Some restrictions apply :)

It uses the fact that Notes counts 5 minutes from the START of the previous instance, not from the END. So if your agent takes 4 minutes 50 seconds to run, the next execution of the agent (assuming perfect conditions) will be just 10 seconds after the previous execution is finished. Delays caused by server load and other unforeseen delays can cause the agent to wait from 30 seconds to 2 minutes until the next execution. During many tests, I havent's observed execution delay longer than 2 minutes.


Even with the worst case 2 minutes delay, it's much better than standard 5 minutes delay. Note that this delay is only for the time between agent executions, within the started agent you can call the function every 2 seconds if you wish.

What if the function takes more than 5 minutes to run? Well, the next instance of the agent will wait until the current instance is finished and then start after a certain delay. In my tests the delay was almost always 2 minutes. The delay if the agent ends 15 seconds BEFORE the 5-minutes period (4m 45s) was according to my tests always 1 minute 5 seconds. These numbers will most certainly be somewhat different on your server.


The main disadvantage of this method is that one of the Agent Manager's threads will be constantly busy, so you would need to increase the number of agent threads to make it possible for other scheduled agents to run too.

To enable your agent to run every 60 seconds, do following:
1) In your existing agent, move the code from Initialize method to MainAction method.
2) Paste the code from the agent below into the Initialize method of your agent.
3) Change variable values as needed.

Agent is configured to execute the MainAction() function every 60 secondss (runinterval), max 100 times (runmaxtimes), during the 5 minute period (agentschedinterval).

Agent execution flow. Click to enlarge.


"Gone in 60 seconds" agent:



Sub Initialize
'Created by Andrei Kouvchinnikov, www.botstation.com

Print "******************* Agent started *******************"

Dim session As New NotesSession
Dim db As NotesDatabase
Dim expectedruntime As Integer 'time which take to run the function. required only for deciding about ending the loop.
Dim runinterval As Integer 'interval (sec) between runs
Dim runmaxtimes As Integer ' max number of times the function will run
Dim runcounter As Integer 'counter of number of times the function run
Dim runstart As Long, runend As Long, rundiff As Long
Dim notimeleft ' loop must be closed now, no time left to run another round
Dim agentstart As Long 'initial time when agent started. required only for deciding about ending the loop.
Dim agentschedule As Integer 'nr of minutes this agent is scheduled to run. required only for deciding about ending the loop.
Dim dynadjust 'instead of hardcoded runtime value, use last run period for calculation of next period
Dim uselongestperiod 'instead of the last run period, use the longest period the function took to run

expectedruntime=10 ' function is initially expected to take max 10 seconds
runinterval=60 ' function is called with interval of X seconds
runmaxtimes=100 'if function's run time variates, you can limit max number of times function runs
agentschedinterval=5 'agent is scheduled to run every X minutes. From the agent properties.
margininterval=5 'nr of seconds to add to the final round. used for situations when function's execution time is 0.
runcounter=0
dynadjust=True
uselongestperiod=True

agentstart=Timer

While runcounter<runmaxtimes And notimeleft=False
runstart=Timer

Call MainAction()

runend=Timer
If dynadjust=True Then expectedruntime=Int(runend-runstart) 'dynamically adjust expected time to actual time it take to run the function
If uselongestperiod Then
If expectedruntime<Int(runend-runstart) Then expectedruntime=Int(runend-runstart)
End If
timeleft=runinterval-expectedruntime

If Int((agentschedinterval*60)-Int(Timer-agentstart))<timeleft+(expectedruntime+Int(expectedruntime/100*30)+margininterval) Then 'assumes that function can take 30% longer time to run than the last time
notimeleft=True
Print "Exit. Function will not manage to finish one more run. Computed time: "+Cstr(Now)+" + "+Cstr((expectedruntime+Int(expectedruntime/100*30)))
End If

If timeleft>0 And notimeleft=False Then
Sleep timeleft 'finished before the expected time. sleep until next execution.
Else
Sleep Int((agentschedinterval*60)-Int(Timer-agentstart))-margininterval 'sleep X seconds-margin
End If
runcounter=runcounter+1
Wend
Print "******************* Agent finished *******************"
End Sub


Sub MainAction()
Print "Triggered "+Cstr(Now)

%REM
Here goes your code
Delete demo code below.
%END REM


Randomize
sleeprand=Int(Rnd()*10)
Sleep sleeprand 'Simulates time taken by the function's operations by sleeping X seconds

End Sub





This LotusScript was converted to HTML using the ls2html routine,
provided by Julian Robichaux at nsftools.com.


Output from the agent. Note the 1m 5s delay between instances.



Technorati:

Thursday, April 05, 2007

Happy Easter!

Happy Easter everyone!


Bible reading for the 21st century: let Sametime bot to choose a random quote from the Bible, and then find that chapter in the book and continue reading from there!

Link to online bot: Random Bible Quote

Monday, February 05, 2007

The fastest way to programmatically import data from Excel to Lotus Notes

There are several ways to import data from Excel to Lotus Notes/Domino. One of the most popular is the built-in Import menu option, which can be used manually and works fine in most cases.

But if you want to import Excel data programmatically, the easiest way is to use Excel's OLE Automation Objects. There are several ways to read data from Excel using OLE, and the one most often mentioned is reading data cell-by-cell. This is a very slow method, and should only be used if you want to read/write special cell properties such color, fonts, etc.

The fastest method I found so far is to read and write Excel data using blocks of data with ExcelSheet.Range method. You can with a single operation read the whole Excel sheet into an array, which extremely efficient, especially if there are many columns.

I have created an example which can be used to import people from Excel to Notes. Sample Excel file can be downloaded here.

Depending on the type of data and number of columns, the speed of this method can be up to 100 times faster than reading cell-by-cell. It imports 100 person documents per second, and the most of this time is used on creating new Notes documents, not on getting data from Excel.

Sub Initialize
'This agent imports person records from Excel to Notes. It uses Range method which makes it very fast.
'Created by Botstation Technologies (www.botstation.com)

Dim session As New NotesSession
Dim db As NotesDatabase
Dim doc As NotesDocument
Dim xlApp As Variant, xlsheet As Variant, xlwb As Variant, xlrange As Variant
Dim filename As String, currentvalue As String
Dim batchRows As Integer, batchColumns As Integer, totalColumns As Integer
Dim x As Integer, y As Integer, startrow As Integer
Dim curRow As Long, timer1 As Long, timer2 As Long
Dim DataArray, fieldNames, hasData

timer1=Timer
filename="C:\people.xls"
batchRows=200 'process 200 rows at a time

Set db=session.CurrentDatabase
Set xlApp = CreateObject("Excel.Application")
xlApp.Visible = True 'set Excel program to run in foreground to see what is happening
Set xlwb=xlApp.Workbooks.Open(filename)
Set xlsheet =xlwb.Worksheets(1)

Redim fieldNames(1 To 250) As String

DataArray=xlsheet.Range("A1").Resize(batchRows, 250).Value 'get worksheet area of specified size

For y=1 To 250 'we assume max 250 columns in the sheet
currentvalue=Cstr(DataArray(1,y))
If currentvalue<>"" Then 'abort counting on empty column
fieldNames(y)=currentvalue 'collect field names from the first row
totalColumns=y
Else
y=250
End If
Next

Redim Preserve fieldNames(1 To totalColumns) As String

curRow=2
hasData=True
While hasData=True 'loop until we get to the end of Excel rows
If curRow=2 Then startrow=2 Else startrow=1
For x=startrow To batchRows
curRow=curRow+1
If Cstr(DataArray(x,1))+Cstr(DataArray(x,2))<>"" Then 'when 2 first columns are empty, we assume that it's the end of data
Print Cstr(curRow-2)
Set doc=New NotesDocument(db)
doc.Form="Person"
doc.Type = "Person"
For y=1 To totalColumns
currentvalue=Cstr(DataArray(x,y))
Call doc.ReplaceItemValue(fieldNames(y), currentvalue)
Next
doc.ShortName=doc.FirstName(0)+" "+doc.LastName(0)
Call doc.save(True, False)
Else
hasData=False
x=batchRows
End If
Next
If hasData=True Then DataArray=xlsheet.Range("A"+Cstr(curRow)).Resize(batchRows, totalColumns).Value 'get worksheet area
Wend
timer2=Timer
Call xlApp.Quit() 'close Excel program

Msgbox "Done in "+Cstr(timer2-timer1)+" seconds"
End Sub


This LotusScript was converted to HTML using the ls2html routine,
provided by Julian Robichaux at nsftools.com.


The Excel sample file was generated using this online tool: data generator

Tags:

Tuesday, January 30, 2007

Programmatically change user's Notes password

To programmatically change user's password, Domino developers can use API function W32_SECKFMChangePassword. The function accepts 3 parameters: path to ID file. old password and new password.
Based on the article on experts-exchange web síte, I have created a LotusScript agent which prompts user for his old password, prompts for the new password, automatically reads the path to current ID file and changes the password for that ID file.
Same code can be used in a LotusScript button mailed to users with instructions to click the button in order to change their current password.

At the end of the script developer might also want to add functionality to send an email to the administrator notifying about successfull or failed password change.


Const NOERROR = &H0
Const ERR_MASK = &H3FFF
Const NULLHANDLE = &H0

'// Lotus Notes/Domino C API (Windows/Intel 32-bit)
Declare Function W32_SECKFMChangePassword Lib {nnotes.dll} Alias {SECKFMChangePassword} ( Byval pIDFile As String , Byval pOldPassword As String , Byval pNewPassword As String ) As Integer
Declare Function W32_OSLoadString Lib {nnotes.dll} Alias {OSLoadString} ( Byval hModule As Long , Byval StringCode As Integer , Byval retBuffer As String , Byval BufferLength As Integer ) As Integer

Sub Initialize
Dim session As New NotesSession
Dim IDFile As String, oldpassword As String, newpassword As String
IDFile=session.GetEnvironmentString( "KeyFilename", True )
oldpassword=Inputbox("Enter old password", "Old Password")
If oldpassword="" Then
Msgbox "No password entered"
Exit Sub
End If
newpassword=Inputbox("Enter new password", "New Password")
If newpassword="" Then
Msgbox "No password entered"
Exit Sub
End If

Call ChangePassword(IDFile,oldpassword,newpassword)
End Sub

Sub ChangePassword( id As String, oldp As String, newp As String)
Dim intAPIResult As Integer
Dim szErrorText As String
Dim szBuffer As String * 1024

intAPIResult = W32_SECKFMChangePassword ( id, oldp, newp )
Stop
If Not ( ( intAPIResult And ERR_MASK ) = NOERROR ) Then
szBuffer = String$ ( Lenb ( szBuffer ) , 0 )
Call W32_OSLoadString ( NULLHANDLE , intAPIResult , szBuffer , Lenb ( szBuffer ) - 1 )
If Instr ( 1 , szBuffer , Chr$ ( 0 ) , 5 ) > 1 Then
szErrorText = Left$ ( szBuffer , Instr ( 1 , szBuffer , Chr$ ( 0 ) , 5 ) - 1 )
Elseif Instr ( 1 , szBuffer , Chr$ ( 0 ) , 5 ) = 0 Then
szErrorText = {}
Else
szErrorText = szBuffer
End If
Messagebox szErrorText , 16 , {C API ERROR CODE: } & Cstr ( intAPIResult )
Else
Msgbox "Successfully changed"
End If
End Sub



Tags: