Show-n-Tell Thursday

Forcing a file name when downloading a dynamically generated csv file

Over the last couple of weeks I’ve been working on a customisation project using IQJam as a starting point and then making it better fit a particular customer’s requirements. It’s worth mentioning as I finally had the justification to spend a little time investigating a problem which has been bugging me.

One of the features I added to IQJam was the ability to export data to Excel, a common enough feature that you’ve been able to do in Domino for ever. Simply print data out to the browser in simple HTML format and change the content-type of the page to “application/vnd.ms-excel”. That’s not the point of this posting really.

The problem I’ve been trying to work around is that if your user is using Excel 2007 or later (I’m only on the beta of Office 2010 but it still seems to be a problem for me anyway) and you use the printing HTML technique, Excel raises an error for the user when they load the page, something like:

The file you are trying to open, ‘name.ext’, is in a different format than specified by the file extension. Verify that the file is not corrupted and is from a trusted source before opening the file. Do you want to open the file now?

According to various technotes and blog posts there is no work around for this, it is a deliberate security feature of Excel. Fair enough I can live with this, though it’s not acceptable to the end user, so instead of generating an Excel file I reverted to generated CSV formatted data.

The thing I learned from my investigations into the Excel problem was that there is this header which you can add to the data being generated which tells the browser that the page being opened is actually a file attachment and what the name of the file you want to download should be. So in my example, I’m able to create a unique file name every time the page is generated and also specify that it should be saved as a CSV file, not .XSP which is what it would be thanks to the page being loaded being an XPage. See this example:

try{
    var exCon = facesContext.getExternalContext();
    var writer = facesContext.getResponseWriter();
    var response = exCon.getResponse();
    response.setContentType(“text/csv”);
    response.setHeader(“Content-disposition”, “attachment; filename=export_” + DateConverter.dateToString(@Now(), “yyyyMMddhhmm”) + “.csv”);
    response.setHeader(“Cache-Control”, “no-cache”);
    writer.write(getCSVBody());
    writer.endDocument();
    facesContext.responseComplete();
    writer.close();
}catch(e){
    _dump(e);
}

So this server side javascript sits in the afterRenderResponse of my XPage and is using the “web agents XPages style” technique which Stephan first documented to generate non HTML content from an XPage. The key line for this blog post is where the “Content-disposition” header is set, hopefully you can see where the filename is being created (I’m also using Tommy Valand’s DateConverter SSJS code to get the current date / time formatted into a nice string).

Anyway, not a new technique looking at the dates on some of the internet postings out there, but a new one on me and worth passing on I thought.

Be very careful with your content type for Chrome Ajax requests

We released the first beta for IdeaJam 1.7 today. There are *tons* of changes under the covers including the new MooTools 1.2 framework, on which you can expect more later this week.

But for the moment, I thought I’d pass along one little tip which we ran into. Inevitably during our own testing we had missed this bug. When you’re in an idea you can “Click to show votes” and what happens behind the scenes is that an Ajax request is fired off to our votes database, an agent runs and send back a list of the votes related to that idea, when they were cast, who by and so on. That agent had been set up with the following code at the start:

Print |Content-Type:text/javascript; charset=utf-8|
Print |Cache-Control: no-cache|
But what we are actually doing is pre-building some HTML and then just inserting it into a div, it saves a little bit of browser processing time this way. But in Chrome we were getting an error:
Uncaught SyntaxError: Unexpected token <
It took a while to track down the problem, but now I found it, it’s obvious. The content-type of the agent needs to actually be set to text/html, not text/javascript. All of the other browsers worked fine with this oversight, even Safari which is a fellow Webkit browser, but Chrome seems to be a lot more picky. So this is just a reminder to make sure you’re very precise with what is being sent to Chrome from your server.

 

XPages: Displaying a view inside a Dojo Dialog

This is my first SnTT post for absolutely ages, but, hopefully I’ll be able to do quite a few more over the next few months as I am just starting a brand new XPages project for a customer that runs through until August.

One of the first things I have been bashing my head against the development wall over is how to get a view to act as a picklist in a dialog. In theory, it should be pretty easy, you just add the “dijit.Dialog” to your XPage resources like so:

Don’t forget to also enable the dojoParseOnLoad and dojoTheme properties at the same time.Then inside your XPage you’ll want to actually create the dialog HTML:

At this point I had assumed that I could just add my view control inside the div and Bob, as they say, would be my mother’s brother. But I hadn’t catered for Dojo taking control of the HTML in the XPage once it was rendered. Basically what happens is that after the page loads, Dojo moves the div I created out of the Domino generated form and makes it a child of the body element. This displays fine, until you try and navigate through the view using the pager control, which no longer works as the javascipt requires the HTML to beinside the Domino form. So as far I can tell you have a couple of options, either move the div back inside the form, but I’m assuming that Dojo moved it out for a reason so I wanted to avoid that.

In the end I moved the view control itself to be in a standalone XPage and then opened that via an iFrame that is embedded inside the dialog div. It’s just a matter then of changing my javascript to refer to window.parent when I want to send values back from the dialog to the main XPage.

Tips for referencing elements in XPages JavaScript

I managed to hit a bit of a wall with my IdeaJam on XPages development this week. Up until now I haven’t really needed to write any client side JavaScript which is impressive in itself compared to how much client side scripts we have in IdeaJam classic. But I have reached that point where I want to add some interactivity without round-tripping to the server.

In classic Domino it’s very easy to write script that refers to specific elements of the HTML because you get to control the IDs of those elements. But with XPages it is not possible to predict what the name of an element will be when it reaches the browser. So you are provided with a couple of functions to help you out. When you’re writing client side Javascript in an XPage client side event (like clicking a button) it’s very easy, the syntax you use to reference an element is “#{id:myElement}” in place of the id. So for example you could use:

dojo.byId(“#{id:myDiv}”).style.display = “none”;

But the thing that I didn’t know about until John Mackey gave me some help was that there is a second way of getting the ID of an element. My specific problem was that I wanted to have a script run when the page finishes loading and that script needed to go and work on several different elements. It’s easy enough to make a script run “onload”. You can actually write JavaScript directly into the source XML of the XPage, so I have added, at the top of my XPage something like this:

<script language=”Javascript”>
XSP.addOnLoad(function () {
initLocal();
}
);
</script>

This will tell Dojo to call the function “initLocal()” when the page has finished loading, no problem there. But my initLocal function needs to reference an element on the page and I can’t use the “#{id:myElement}” format as that only gets evaluated in events that are provided for you by the XPage, enter the “getClientId()” function. You can add a computed field to the XPage that has JavaScript embedded within it in the following format:

var out=”<script language=\”Javascript\”>\n”;
out += “var myElementId = \”” + getClientId(“myElement”) + “\”;\n”;
out += “function initLocal(){\n”;
out += “myFunction(myElementId);\n”;
out += “}\n”;
out += “</script>”;
out

And by the time it makes it out the the browser it has been translated into:

<script language=”Javascript”>
var myElementId = “view:_id1:_id2:content:_id4:myElement”;
function initLocal(){
myFunction(myElementId);
}
</script>

So in the initLocal function I call another function which is held in a Client Side JavaScript library and I pass in the element ID so that the function can reference the element on the page with proper separation between UI and business logic. So it’s all a bit round the houses but this way of doing things does work well and hopefully you won’t need to go through the pain that I have in working this stuff out.

Finally a big thanks to John Mackey for his help, this is when the Lotus community really proves it’s worth

Character encoding in an Ajax LotusScript agent

I know it’s Monday, but I thought I’d share this with you anyway.

So, most people know that you can control the content-type in an agent called by an Ajax request (or any type of agent for that matter) by using this line:

print |content-type: text/plain|

which will result in plain text being sento to the browser, of course you can also send HTML, XML, JSON or any other content type that you so choose.

What you may not have known is that you can also control the charset property of the response. Why would you want to do this? Well if you have an agent that returns high number Ascii characters such as é or ö then by default they will be returned to the Ajax request as non-printing characters. But if you use this line in your agent then they will be transferred to your browser correctly:

print |content-type: text/plain; charset=utf-8|

So my recommendation is that for every agent that you write where you would have just set the content-type, just take that extra few seconds to define the character set as well to save yourself trouble down the line.

You may well take from this, that I have just been burned by this very issue, and you may well be correct. Did I mention that version 1.3 of IdeaJam will be released very shortly 😉

Using easymock for disconnected Domino Java testing

One of the projects I’m working on at the moment has a *lot* of code written in a J2EE application which talks to Oracle using JDBC and Domino over CORBA. It actually all works really well but the approach of one of our non-Domino developers was really interesting to me, so I asked if I could write about it up here.

So the approach that Greg takes is test driven development, that is he writes his tests before writing the code. For his testing he would rather not connect to the Domino server all the time, but instead mock up objects to make sure that his other code works correctly. Enter EasyMock, which is a JAR file which you can include in any project that basically pretends to be whatever object you want it to be.







package test;

 

import junit.framework.*;

import java.util.Vector;

import org.easymock.EasyMock;

import org.easymock.*;

import static org.easymock.EasyMock.*;

 

import lotus.domino.*;

import com.agcs.cores.utilities.*;

 

 

public class testDominoFieldProcessing extends TestCase {

 

  Document mock = null;

  DateTime datemock = null;

  DominoFieldProcessing dfield = null;

  public void setUp() throws Exception

  {

 

    mock = createMock(Document.class);

 

    Vector<DateTime> v = new Vector<DateTime>();

    for (int i = 0; i< 3;i++)

    {

      datemock = createMock(DateTime.class);

      expect(datemock.getLocalTime()).andReturn("01:02:03");

      expect(datemock.toJavaDate()).andReturn(new java.util.Date(System.currentTimeMillis()   ));

      v.add(datemock);

      replay(datemock);

    }

 

    expect(mock.getItemValue("time")).andReturn(v);

    expect(mock.hasItem("time")).andReturn(true);

    replay(mock);

 

  }

 

 

  public void testNotesDateToJavaDate()  throws Exception

  {

    for (java.util.Date d:  DominoFieldProcessing.getFieldValueAsListDate(mock,"time"))

      System.out.println(d);

  }

}


Java2html


What this code shows is the use of EasyMock to be a Notes Document and a DateTime Object. You tell it what the methods that you want to call should return, and then, for all intents and purposes, the object is a Domino object but without having to go through the hassle of deploying your code to a server which has the rights to connect to a proper Domino server. Now of course this should only be used early on in the development lifecycle, but for environments where connecting to Domino can be difficult this is a real time saver.

WebDAV tip

I spent a little time last night getting WebDAV enabled on a database and server so that I could upload the latest version of Dojo as file resource design elements. Unfortunately it took a little more time than expected. Of course Jake has an article on how to set it up but the one thing which I missed for about half an hour of swearing was that you must check the design locking box in the database properties as well as everything else. The Administration documentation is ambiguous on this point from my view.

So hopefully if people are seeing the message “The HTTP method is not allowed for the specified URL” when trying to copy a file into the Domino database using Windows Explorer (or indeed the OS X Finder although the error message is somewhat less helpful there!) then a quick Google should find this page.

LotusScript to Make a new web user able to log in immediately

Bill offered some advice yesterday about a problem I’ve been having with new user registrations for Defectr.

What was happening was that a new user would register, everything would work fine but it took up to 15 minutes for the new account to work. I thought I had tried all combinations of solution but am also running the Defectr registrations in a Directory Assistance NAB (ie not the primary NAB on the server) so assumed that that was the source of the problem. But Bill’s posting spurred me to have another quick look and it seems like I’ve got it fixed now.

All of the work I had been doing was around refreshing views in the NAB in which the users were being created, it never occurred to me to refresh the same views in the main NAB but when I did the new account worked straight away. I have no idea why this would be the case, any idea you admin types?

Anyway, this is the code I now run when a new user registers:

Option Public Option Declare Use "OpenLogFunctions" Use "slnnotesdll" Sub UpdateNAB On Error Goto Whoops Dim commander As New nnotesdll Dim sess As New NotesSession Dim strError As String Dim server As String Dim scommand As String Dim strResponse As String Server = sess.CurrentDatabase.Server sCommand = |load updall names.nsf -t "($ServerAccess)" -r| strResponse="" strError=commander.rConsole(Server,sCommand,strResponse) Sleep 1 sCommand = |load updall names.nsf -t "($Users)" -r| strResponse="" strError=commander.rConsole(Server,sCommand,strResponse) Sleep 1 sCommand = |dbcache flush| strResponse="" strError=commander.rConsole(Server,sCommand,strResponse) Sleep 1 sCommand = "Tell Adminp Process People" strResponse="" strError=commander.rConsole(Server,sCommand,strResponse) Sleep 1 sCommand = "Show nlcache reset" strResponse="" strError=commander.rConsole(Server,sCommand,strResponse) Exit Sub Whoops: Call LogError() gResponse = "ERROR: " + Error Exit Sub End Sub
This LotusScript was converted to HTML using the ls2html routine,
provided by Julian Robichaux at nsftools.com.

Tags: Show-N-Tell Thursday

LotusScript Class to Convert Numbers to Words in French

A bit of an obscure one this, but we are making an English application multi lingual. One function converts numbers to words, and having tried to use the same logic for the French translations it just doesn’t work so I spent a couple of hours this morning porting a Java class to work in LotusScript to perform the function:

'**********************************************************
'*Ported from Java Class found at:
'*http://www.rgagnon.com/javadetails/java-0426.html
'**********************************************************
Class FrenchDecimalFormat
  tenNames() As String
  uniteNames1() As String
  uniteNames2() As String
  
  Sub new()
    Redim tenNames(9)
    tenNames(0) = ""
    tenNames(1) = ""
    tenNames(2) = "vingt"
    tenNames(3) = "trente"
    tenNames(4) = "quarante"
    tenNames(5) = "cinquante"
    tenNames(6) = "soixante"
    tenNames(7) = "soixante"
    tenNames(8) = "quatre-vingt"
    tenNames(9) = "quatre-vingt"
    
    Redim uniteNames1(19)
    uniteNames1(0) = ""
    uniteNames1(1) = "un"
    uniteNames1(2) = "deux"
    uniteNames1(3) = "trois"
    uniteNames1(4) = "quatre"
    uniteNames1(5) = "cinq"
    uniteNames1(6) = "six"
    uniteNames1(7) = "sept"
    uniteNames1(8) = "huit"
    uniteNames1(9) = "neuf"
    uniteNames1(10) = "dix"
    uniteNames1(11) = "onze"
    uniteNames1(12) = "douze"
    uniteNames1(13) = "treize"
    uniteNames1(14) = "quatorze"
    uniteNames1(15) = "quinze"
    uniteNames1(16) = "seize"
    uniteNames1(17) = "dix-sept"
    uniteNames1(18) = "dix-huit"
    uniteNames1(19) = "dix-neuf"
    
    Redim uniteNames2(10)
    uniteNames2(0) = ""
    uniteNames2(1) = ""
    uniteNames2(2) = "deux"
    uniteNames2(3) = "trois"
    uniteNames2(4) = "quatre"
    uniteNames2(5) = "cinq"
    uniteNames2(6) = "six"
    uniteNames2(7) = "sept"
    uniteNames2(8) = "huit"
    uniteNames2(9) = "neuf"
    uniteNames2(10) = "dix"
  End Sub
  
  Function convertZeroToHundred(number As Integer) As String
    Dim laten As Integer
    Dim lUnite As Integer
    Dim result As String
    Dim joiner As String
    laten = Fix(number / 10)
    lUnite = number Mod 10
    result = ""
    
    If laten = 1 Or laten = 7 Or laten = 9 Then
      lUnite = lUnite + 10
    End If
    
    joiner = ""
    If laten > 1 Then
      joiner = "-"
    End If
    If lUnite = 0 Then
      joiner = ""
    Elseif lUnite = 1 Then
      If laten = 8 Then
        joiner = "-"
      Else
        joiner = " et "
      End If
    Elseif lUnite = 11 Then
      If laten = 7 Then
        joiner = " et "
      End If
    End If
    If laten = 0 Then
      result = uniteNames1(lUnite)
    Elseif laten = 8 Then
      If lUnite = 0 Then
        result = tenNames(laten)
      Else
        result = tenNames(laten) + joiner + uniteNames1(lUnite)
      End If
    Else
      result = tenNames(laten) + joiner + uniteNames1(lUnite)
    End If
    
    
    convertZeroToHundred = result
  End Function
  
  Function convertLessThanOneThousand(number As Integer)
    Dim hundreds As Integer
    Dim remainder As Integer
    Dim strRemainder As String
    Dim result As String
    hundreds = Fix(number / 100)
    remainder = number Mod 100
    strRemainder = convertZeroToHundred(remainder)
    
    If hundreds = 0 Then
      result = strRemainder
    Elseif hundreds = 1 Then
      If remainder > 0 Then
        result = "cent " + strRemainder
      Else
        result = "cent"
      End If
    Else
      If remainder > 0 Then
        result = uniteNames2(hundreds) + " cent " + strRemainder
      Else
        result = uniteNames2(hundreds) + " cents"
      End If
    End If
    convertLessThanOneThousand = result
  End Function
  
  Function convert(number As Double)
    Dim snumber As String
    Dim billions As Integer
    Dim millions As Integer
    Dim hundredthousands As Integer
    Dim thousands As Integer
    Dim strBillions As String
    Dim strMillions As String
    Dim strHundredThousands As String
    Dim strThousands As String
    Dim result As String
    If number = 0 Then
      convert = "zéro"
      Exit Function
    End If
    
    snumber = Cstr(number)
    snumber = Format$(number, "000000000000")
    billions = Cint(Left$(snumber, 3)) ' XXXnnnnnnnnn
    millions  = Cint(Mid$(snumber, 4,3)) ' nnnXXXnnnnnn
    hundredthousands = Cint(Mid$(snumber, 7, 3))  ' nnnnnnXXXnnn
    thousands = Cint(Mid$(snumber, 10, 3))  ' nnnnnnnnnXXX
    
    If billions = 0 Then
      strBillions = ""
    Elseif billions = 1 Then
      strBillions = convertLessThanOneThousand(billions) + " milliard "
    Else
      strBillions = convertLessThanOneThousand(billions) + " milliards "
    End If
    result =  strBillions
    
    If millions = 0 Then
      strMillions = ""
    Elseif millions = 1 Then
      strMillions = convertLessThanOneThousand(millions) + " million "
    Else
      strMillions = convertLessThanOneThousand(millions) + " millions "
    End If
    result = result + strMillions
    
    If hundredthousands = 0 Then
      strHundredThousands = ""
    Elseif hundredthousands = 1 Then
      strHundredThousands = "mille "
    Else
      strHundredThousands = convertLessThanOneThousand(hundredthousands) + " mille "
    End If
    result = result + strHundredThousands
    
    strThousands = convertLessThanOneThousand(thousands)
    result =  result + strThousands
    
    convert = result
  End Function
End Class&#0;&#0;&#0;

Tags: Show-N-Tell Thursday

Posting to a Form for AJAX validation

I am guessing that this has been done before elsewhere but it was a new idea for me anyway.

A couple of weeks ago Julian and Philippe were talking about using the Scriptaculous Autocomplete feature. Philippe had the cracking idea of using a page to do DBLookups on the fly to provide JSON data.

While on the train this morning I came up with the idea of performing complex form validation using a similar technique. The sort of thing I am thinking of is uniqueness checking, complex business rules and so on but without the overhead of firing up an agent to do the processing, submitting to a form will do that same task much faster.

It’s definitely worth having a read of Philippe’s article that I mentioned earlier. What I am proposing is that rather than running a "GET" request from a page which he suggested for the autocomplete tool, that we create a simple form which we "POST" to, to make sure that all of the fields are valid, before submitting the "real" form to be saved. Let me describe things in a little more detail. I have a form which is registering a new user for an application. I need to make sure that the company name of the user is valid and that his email address has not been used for any other users that are already registered. Now this is pretty simple stuff to handle with normal form validation or webquerysave agents but I am not a big fan of the user submitting, waiting for a screen refresh only then to be told they’ve got something wrong.

So instead, before submitting the form, we run some javascript which creates an AJAX request (as usual I am using Prototype but you can use your preferred framework). My submit function looks something like this:

function submitClient() {
//Now we do the server side validation
var a=$H(
{
clientnameandcode: $F("clientnameandcode"),
email: $F("email")
}
);
var myAjax = new Ajax.Request(
'/validateNewclient?createdocument&rand=' + Math.floor(Math.random()*1001),
{method: 'post', parameters: a.toQueryString(), onComplete: submitclientContinue}
);
}
function submitclientContinue(jsonDoc) {
if (jsonDoc.responseText == "\n") {
//All the validation has been passed so submit properly
document.forms[0].submit();
}else{
//There are error messages so display them in a div on the form
$("validation").innerHTML = "

" + jsonDoc.responseText +"

"; $("validation").style.display = ""; } }

The two fields I am interested in, clientnameandcode and email are posted to the form called "validateNewClient". The form then has the following design:

The things to note are…

  • The SaveOptions field which is set to "0" to prevent the document being saved
  • The two data fields which match the parameters sent in by the AJAX request
  • The $$Return formula which actually does all of the validation work, it just computes some response messages where there are errors, or empty string for successes.

    Now the only "hack’ I’ve had to do is with the return value from the $$Return, where because @SetHTTPHeader("Content-Type"; "text/text") doesn’t work I have to redirect the message I want to return to a page where I can format the message as simple text to be returned to the AJAX request.

    This isn’t an approach which I would recommend for every situation, but where you have relatively complex business rules which need to be validated and the network can handle the extra load etc then I think it’s quite neat.

    Of course the one caveat is that with client side validation you must always, always double check on the server side to prevent sneaky people circumventing all of the Javascript you have written to make their lives easier!

    Technorati Tags: Show-N-Tell Thursday